feat(topbar): «Что нового» button + 2026-05-12 state.md update

This commit is contained in:
Александр Зимин
2026-05-12 17:05:24 +00:00
parent 58dd871808
commit a34c6d4c82
21 changed files with 1853 additions and 8 deletions
+184
View File
@@ -0,0 +1,184 @@
# Состояние разработки Ordinis (ЦУОД ОДХ)
**Версия:** v2.14.0 (mega-sprint feature wave: 38 MRs за 2 дня, prod v2.8.0 → v2.14.0)
**Дата:** 2026-05-12 (vespertine update — конец дня 2)
**Owner:** zimin.an@nstart.space
**Окружения:**
- **prod** (cluster.264, https://ordinis.k8s.264.nstart.cloud, ✅ LIVE v2.14.0, Альтум traffic stable)
- **staging** (cluster.265, dev environment)
**Предыдущий snapshot:** [2026-05-07-state.md](./2026-05-07-state.md) (v1.0.1, post-cutover hardening)
---
## 0. Что изменилось с v1.0.1 (2026-05-07 → 2026-05-12)
### Mega-sprint feature wave
**День 1 (2026-05-11):** !108!140 = 33 MRs merged (28 features + 3 chores + 1 closed zombie + 1 v2.11.x deploy pending). Tags v2.0.0 → v2.11.0. Highlights: backend pending endpoints (5/5 + extras), schema workflow API (8 endpoints) + Phase 1a/b/c UI, History tab wiring, TimeTravelModal fully wired, Monaco editor, friendly error UI, webhook stats dashboard.
**День 2 (2026-05-12):** !141!147 = 7 MRs (5 merged + 2 design-only open). Tags v2.12.0 → v2.14.0. Focus shifted: feature shipping → polish + design proposals.
### День 2 detail (focus этого state.md)
**Polish features shipped:**
- **!141 → v2.12.0** RecordDrawer chip strip scroll-spy (IntersectionObserver active section highlight)
- **!142 → v2.13.0** Searchable MultiSelect через cmdk + DateRangePicker (react-day-picker, lazy chunk 67kb gz)
- **!143 → v2.14.0** Webhook delivery histogram (SVG stacked bar chart, 24h/7d toggle, hover tooltip)
**Design proposals (3 docs, не implementation):**
- **!143 + !144** Redis FK projection design v1 → v2.1 после 2 rounds /plan-eng-review. **NEAR-CLEAR.** Sprint estimate 8.5d → 4-5d после `LineageIndexService` reuse decision. 15-test plan + 3 critical safety mitigations + 3-tier kill switch + explicit SLO targets.
- **!145 + !146** AI schema assist design v1 → v2 после /office-hours. **Approach C-revised (hybrid template + per-field LLM)** chosen over v1 whole-schema LLM. 7B model на consumer GPU. P5 measurement step как prerequisite. 3-4d implementation.
- **!145 + !147** Dictionary marketplace design v1 → v2 после /office-hours + cross-model challenge. **Approach C-revised (bundle hygiene 2-3d hard cap)** chosen over v1 full marketplace (10-14d). Java handoff readiness + sales artifact angle. Anti-creep enforcement language.
**Prod deploy v2.14.0:**
- Pipeline 6700 (manual API trigger on tag)
- Bridge `trigger-deploy-prod` (id=30851) — played
- Downstream pipeline 6708 в ordinis-infra (project 368)
- Manual gate `deploy-prod` (id=30893) — played
- Helm rollout ~70s, no rollback
- Verified: `curl https://ordinis.k8s.264.nstart.cloud/api/v1/version``{tag: "v2.14.0", commit: "08c92627"}`
### 0.1. New design proposals (decision artifacts)
Все три ждут measurement / customer event / GPU availability перед implement:
| Design doc | Status | Effort | Prerequisite | Defer rationale |
|---|---|---|---|---|
| `docs-internal/design/redis-fk-projection.md` v2.1 | CLEAR | 4-5d | Dict упрётся в latency SLA | Premature optimization, текущий read path handles RPS |
| `docs-internal/design/ai-schema-assist.md` v2 | CHOSEN (Approach C) | 3-4d | Baseline measurement 2 weeks + GPU verification | Demand evidence пока ZERO — founder-driven feature |
| `docs-internal/design/dictionary-marketplace.md` v2 | CHOSEN (Bundle hygiene C-revised) | 2-3d hard cap | Customer #2 ETA + signing key infra decision | Bundle hygiene proceeds Q3, full marketplace ждёт N=2 customers |
**Design docs moved** из `docs/design/``docs-internal/design/` per /docs/ scope discipline (public docs = integrator + operator + reviewer only, no engineering internals).
`/office-hours` formal docs:
- `~/.gstack/projects/claude/zimin-main-design-ai-schema-assist-20260512.md`
- `~/.gstack/projects/claude/zimin-main-design-bundle-hygiene-20260512.md`
---
## 1. Стек / архитектура (без изменений с 2026-05-07)
Backend: Java 21 + Spring Boot 3.4.2 + JPA + Liquibase + PostgreSQL 18 + PostGIS + Kafka + Caffeine + Redis. Микросервисы: ordinis-app (writer), ordinis-read-api, ordinis-projection-writer, config-server. Outbox + Kafka publisher pattern, idempotency keys, audit log, valid_from/valid_to + EXCLUSION CONSTRAINT, data_scope filtering.
Frontend: React 19 + Vite 7 + TanStack Router + TanStack Query + Tailwind v4 + Radix primitives + cmdk + sonner toaster + @phosphor-icons/react + Monaco editor (lazy) + react-day-picker (lazy). Custom UI primitives только. @nstart/ui dropped (Stage 3.9).
Auth: Keycloak OIDC + JWT scope-based RBAC. `realm_access.roles` → DataScope (PUBLIC/INTERNAL/RESTRICTED). audience optional.
Infra: cluster.264 prod / cluster.265 staging. CNPG для PostgreSQL (single-instance, daily backups к S3 через geoportal-minio). Strimzi Kafka KRaft single broker. Vault (k8s auth). Redis для projection (per-dict opt-in feature flag).
---
## 2. Functional coverage matrix (updated)
### 2.1. UI features ✅ live на v2.14.0
| Feature | First shipped | Polish | Status |
|---|---|---|---|
| Schema workflow (draft → review → publish) UI | v2.5.0-v2.8.0 (Phase 1a/b/c) | — | ✅ live |
| TimeTravel modal (changes / records / schema tabs) | v2.10.0-v2.11.0 | — | ✅ live |
| History tab + Compare modal | v2.5.0 | — | ✅ live |
| Monaco editor для schema authoring | v2.8.0 | — | ✅ live |
| /reviews tabs (Записи / Схемы) | v2.8.0 | — | ✅ live |
| Friendly error UI (QueryErrorState, 10 sites) | v2.10.0 | — | ✅ live |
| Webhook stats cards (Pending/Retry/Delivered/DLQ + success rate) | v2.9.0 | — | ✅ live |
| Webhook stats histogram (SVG, 24h/7d) | v2.14.0 | — | ✅ live |
| RecordDrawer toolbar (search полей + только заполненные + scroll-spy chip strip) | v2.5.0-v2.12.0 | active section highlight | ✅ live |
| cmdk searchable MultiSelect (webhook events) | v2.13.0 | — | ✅ live |
| DateRangePicker (audit page) | v2.13.0 | — | ✅ live |
| Dark theme sweep (bg-white → bg-surface) | v2.6.0 | — | ✅ live |
| Mobile responsive (Sheet sidebar) | previously done | — | ✅ live |
### 2.2. Backend APIs ✅ live на v2.14.0
| Endpoint group | First shipped | Status |
|---|---|---|
| Dictionary CRUD + bitemporal records | v0.x | ✅ live |
| Schema workflow API (8 endpoints + audit) | v2.2.0 | ✅ live |
| Full timeline `/changelog` с kind discrimination | v2.3.x | ✅ live |
| Diff endpoint `/changelog/{entryId}/diff` | v2.4.0 | ✅ live |
| PATCH `/drafts/{id}` (edit без recreate) | v2.7.0 | ✅ live |
| Webhook subscriptions CRUD + DLQ + replay | v1.x | ✅ live |
| **Webhook stats `/subscriptions/{id}/stats?bucketBy=hour\|day`** | v2.14.0 | ✅ live (NEW) |
### 2.3. Что НЕ shipped (design only)
| Feature | Doc | Status | Blocker |
|---|---|---|---|
| Redis FK projection cascade invalidation | `docs/design/redis-fk-projection.md` v2.1 | CLEAR after 2 reviews | Dict latency SLA |
| AI schema assist (template + per-field LLM) | `docs/design/ai-schema-assist.md` v2 | Approach C chosen | P5 measurement + GPU |
| Bundle hygiene (manifest + publisher CLI, NO UI) | `docs/design/dictionary-marketplace.md` v2 | Approach C-revised chosen | Customer #2 ETA + signing key infra decision |
---
## 3. Backlog state (post-mega-sprint)
### 3.1. Active assignments (этой недели / следующих 2 weeks)
1. **P0 metric для AI schema baseline** — add `admin.schema_authoring.seconds` в `DictionaryDefinitionService.createDraft()`. Ship в v2.15.0 (~1h work). **Critical:** без этого нельзя decide P50 demand evidence для AI feature.
2. **2-weeks baseline collection** на v2.14.0 prod. После — decision: P50 > 20 min → implement AI schema assist Phase 1 (3-4d), P50 < 10 min → defer AI, ship bundle hygiene (2-3d).
3. **GPU verification** в k8s prod cluster (parallel с baseline collection). Talk to ops, confirm vLLM availability.
4. **Bundle signing key infrastructure decision** с corp ops (GitLab CI variables protected vs Vault vs HSM). Document choice.
5. **Customer #2 ETA confirm** — Альтум integration milestone, гидрометео preliminary discussion status. Informs marketplace urgency (не gates implementation though — bundle hygiene proceeds regardless).
### 3.2. Deferred (low priority)
| # | Item | Effort | Блокер |
|---|---|---|---|
| 1 | Phase 1d Keycloak roles в `useCanReviewSchema/Publish` | ~30 мин | infra не пробросила `realm_access.roles` в JWT |
| 2 | GitHub mirror sync (`git subtree split --prefix=ordinis-admin-ui`) | 1 мин | 2 weeks v2.14.0 stability observation |
| 3 | nsi-hot.md update v2.8.0 → v2.14.0 | 10 мин | low priority maintenance |
### 3.3. Roadmap (governance / data quality / federation, 2-3 quartals)
Без изменений с 2026-05-05-state.md:
- Approval workflow v2 (multi-step approval chain, BPM Camunda integration)
- Lineage / dependency graph
- Geo first-class (bbox/polygon query)
- Cross-dictionary FK cascade rules
- Data quality rules engine (cross-field constraints, ML-anomaly detection)
- Federation MDM-кластеров (master/replica)
- Schema registry (Apicurio/Confluent) для Kafka
- Webhooks для не-Kafka consumer'ов
---
## 4. Open risks (updated)
1. **Customer #2 acquisition velocity** — marketplace + AI assist designs assume timeline. Если customer #2 не появляется в Q3 — design docs sit, no rework needed но opportunity cost real.
2. **P5 measurement may invalidate AI assist hypothesis.** Baseline P50 < 10 min → 30мин «admin pain» субъективна, feature defer'нется. Cost = ~1h metric work, low risk.
3. **GPU availability в prod cluster** uncertain. Если нет — AI schema assist requires either: external API за feature flag (compromise data perimeter для non-classified), procurement new GPU node (delay weeks-months), or skip feature entirely.
4. **Bundle hygiene scope creep risk.** Hard cap 2-3 days с anti-creep enforcement language — but founder discipline ultimate enforcement. PR review rule: reject additions outside «what ships» list.
5. **CHANGELOG.md bundling в frontend** (v2.14.0 «Что нового» feature) — main bundle вырос 394kb → 427kb (+33kb / +13kb gz). CHANGELOG растёт ~500 bytes per release. Через 100 releases это ~50kb extra. Acceptable now, может потребовать lazy-load или backend endpoint в будущем.
---
## 5. Naming / branding state
- Repo: `ordinis` (was `nsi`)
- Product display: «Ordinis» английский / «Ордин» русский (rare)
- Customer #1: ЦУОД ОДХ (anchor)
- Bundle ID convention: `<domain>.<vertical>.<bundle-name>` (e.g. `ru.cuod.dzz-ground-segment`)
- Tag scheme: semver `vMAJOR.MINOR.PATCH`, auto-bumped via semantic-release
- Prod URL: https://ordinis.k8s.264.nstart.cloud
- Swagger: https://ordinis.k8s.264.nstart.cloud/swagger-ui.html
- Docs: https://ordinis.k8s.264.nstart.cloud/docs/ (mkdocs)
---
## 6. Workflow patterns (operational learnings, this session)
1. **GitLab deploy gate sequence:** trigger pipeline → play `trigger-deploy-prod` bridge → play downstream `deploy-prod` job. Three separate explicit confirmations.
2. **`GITLAB_HOST=git.nstart.cloud`** env var нужен для `glab api` calls (default goes к gitlab.com). `glab mr view` works without (uses git remote).
3. **Manual API pipeline trigger** для prod deploy (semantic-release bot pipelines skip CI). `glab api -X POST projects/367/pipeline?ref=v{TAG}`.
4. **Cross-model challenge** in /office-hours Phase 3.5 critical — каждый раз когда engaged surface'нул substantial issues missed by single-model reasoning. Marketplace session was strongest example: my pure-defer recommendation overcorrected, subagent caught 4 misses, final synthesis better than either pure position.
---
## Резюме
- **Prod v2.14.0 live** на cluster.264. 6 minor versions shipped в один день (v2.8.0 → v2.14.0). 38 MRs total за 2 дня.
- **Three design proposals documented** с explicit prerequisites + measurement steps + hard caps. None implementation. Defer recommendations stand.
- **Главный риск** для дальнейшего product development — не technical, а demand signal. AI assist / marketplace требуют customer pull или measurement evidence перед dev investment.
- **Next concrete action:** ship metric `admin.schema_authoring.seconds` в v2.15.0 (~1h work), затем 2 weeks observation.
**Прогноз до next major release decision (v3 vs v2.x):** ~2-4 weeks pending baseline measurement results + customer #2 conversation outcomes.
+88
View File
@@ -0,0 +1,88 @@
# Что нового в Ordinis
Список значимых обновлений продукта на понятном языке. Технических подробностей нет — для них есть [внутренний CHANGELOG](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/blob/main/ordinis-admin-ui/CHANGELOG.md) с привязкой к коммитам.
Обновления приходят на твой сервер автоматически при следующем deploy.
## [2.14.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.14.0) (2026-05-12)
### ✨ Новые возможности
* **График доставки webhook'ов** — на странице webhook-подписки теперь видно историю доставок за последние 24 часа или 7 дней. Можно навести курсор на любой столбик и увидеть, сколько было успешных доставок, retry, и сколько ушло в DLQ.
## [2.13.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.13.0) (2026-05-12)
### ✨ Новые возможности
* **Поиск в фильтре типов событий** — у webhook event-type фильтра появилось поле поиска. Можно набрать «Record» и увидеть только относящиеся типы событий, вместо ручного скроллинга через все 11.
* **Выбор диапазона дат** — в журнале аудита вместо двух отдельных полей «с» и «по» теперь один календарь, в котором можно отметить начало и конец периода. Удобнее и компактнее.
## [2.12.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.12.0) (2026-05-12)
### ✨ Новые возможности
* **Подсветка активной секции в редакторе записи** — когда в редакторе записи прокручиваешь форму, соответствующий «таб» в верхней панели подсвечивается синим. Удобно, чтобы не теряться в длинных формах.
## [2.11.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.11.0) (2026-05-12)
### ✨ Новые возможности
* **TimeTravel — реальные данные на момент** — во вкладках «Записи на момент» и «Структура и поля» в окне TimeTravel теперь видны фактические данные, а не заглушка «backend pending». Можно посмотреть, как выглядел справочник в любую дату из истории.
## [2.10.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.10.0) (2026-05-12)
### ✨ Новые возможности
* **Что изменилось — вкладка с реальными изменениями** — в окне TimeTravel вкладка «Что изменилось» теперь показывает фактический diff между версиями (добавленные поля, изменённые типы, удалённые свойства), а не пустую заглушку.
### 🐛 Исправления
* **Понятные сообщения об ошибках** — вместо технического «AxiosError: Request failed with status code 503» теперь видишь нормальное «Сервер временно недоступен» с кнопкой «Повторить». Это работает для всех ошибок 4xx/5xx и проблем с сетью.
## [2.9.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.9.0) (2026-05-12)
### ✨ Новые возможности
* **Статистика webhook-подписок** — на странице подписки появились карточки со счётчиками: в очереди, retry, доставлено, в DLQ + общий процент успешных доставок. Видно с одного взгляда, насколько здоров твой webhook.
## [2.8.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.8.0) (2026-05-12)
### ✨ Новые возможности
* **Редактор схемы прямо в окне создания draft'а** — больше не нужно открывать отдельную форму. Окно «Создать schema draft» теперь содержит полноценный JSON-редактор (Monaco) с подсветкой синтаксиса и проверкой схемы налету.
* **Согласования — отдельные вкладки** — в разделе «Согласования» (`/reviews`) появились вкладки «Записи» и «Схемы» со счётчиками. Auto-switch на ту вкладку, где есть что согласовать.
* **Редактирование draft'а без пересоздания** — если ревьюер запросил изменения, можешь поправить schema draft inline, без удаления и создания нового. Изменения автоматически возвращают draft в статус «На доработке у автора».
* **Фильтр типов webhook-событий** — вместо ввода CSV-строкой теперь multi-select с 11 известными типами событий (`RecordCreated`, `SchemaDraftPublished` и др.).
## [2.7.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.7.0) (2026-05-12)
### ✨ Новые возможности
* **Schema workflow Phase 1c** — окно создания schema draft, расширение раздела согласований, контроль прав доступа.
## [2.6.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.6.0) (2026-05-12)
### ✨ Новые возможности
* **Боковая панель schema draft с действиями** — drawer показывает текущий статус draft'а и контекстные кнопки: отправить на ревью, approve, request changes, reject, publish, withdraw, edit. Видишь только те действия, которые доступны на текущем шаге.
## [2.5.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.5.0) (2026-05-12)
### ✨ Новые возможности
* **Вкладка «История версий»** — теперь показывает реальный timeline изменений с иконками по типу события и кнопкой «Сравнить» — открывает модальное окно с before/after JSON. Раньше там была заглушка.
## [2.4.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/-/tags/v2.4.0) (2026-05-12)
### ✨ Новые возможности
* **Полное сравнение версий схемы** — endpoint позволяет получить before/after любой пары версий схемы для конкретной записи (используется кнопкой «Сравнить» в History tab'е).
### 🐛 Исправления
* **Слайдер TimeTravel** — пометки дат больше не наезжают друг на друга при близких версиях.
* **Кнопки в редакторе записи** — кнопка «JSON» теперь видна явно (раньше скрывалась как иконка). Если у записи всего одна версия, показывается подсказка вместо disabled кнопок.
## [2.3.0-2.0.0](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/blob/main/CHANGELOG.md) (2026-05-12)
Большой релиз-волна: backend endpoints для полного timeline истории, schema workflow API (8 endpoints с maker-checker согласованием), Diff endpoint, тёмная тема. Подробности — в [полном CHANGELOG](https://git.nstart.cloud/2-6/2-6-4/terravault/ordinis/blob/main/CHANGELOG.md).
+49 -7
View File
@@ -1,16 +1,48 @@
# Ordinis НСИ ЦУОД ОДХ — Integrator Guide
# Ordinis НСИ ЦУОД ОДХ
Документация для команд, интегрирующих свои сервисы с Ordinis НСИ ЦУОД.
Сюда не входят operator runbooks и архитектурные deep-dives — те материалы
во внутренней документации проекта.
Документация продукта Ordinis. Здесь — материалы для трёх ролей:
**Anchor v1 — production LIVE на cluster.142** (с 2026-05-07).
- **Операторы справочников** — заводят и поддерживают данные
- **Ревьюеры schema drafts** — принимают решения об изменениях схем
- **Разработчики-интеграторы** — подключают свои сервисы к API
Архитектурные deep-dive'ы, runbook'и для ops-команды и project state —
во внутренней документации проекта (не публикуется).
**Anchor v1 — production LIVE на cluster.264** (с 2026-05-07).
Альтум consumers подключены, blue/green cutover завершён.
## С чего начать
{% list tabs %}
- Я оператор справочников
Заводишь записи (спутники, наземные станции, типы антенн)? Начни с
[руководства оператора](operator/index.md).
Самое полезное:
- [Создание справочника](operator/create-dictionary.md) — если справочника
ещё нет, нужно создать новый
- [Работа с записями](operator/edit-record.md) — добавить, изменить,
найти запись
- [Закрытие записи](operator/close-record.md) — корректно «удалить»,
сохранив историю
- [Разбор ошибок](operator/errors.md) — что значит сообщение, которое
вылетело
- Я ревьюер schema drafts
Принимаешь решения об изменении схем справочников? Смотри
[руководство ревьюера](reviewer/index.md) и
[пошаговую инструкцию ревью](reviewer/schema-drafts.md).
Главное помнить:
- Не ломать существующие записи (breaking changes — через миграцию)
- Не ломать downstream (Альтум, BI) — согласуй, если меняешь поле,
которое они читают
- Документируй причину approve/reject в комментарии (это аудит)
- Я интегрирую свой сервис
Начни с [обзора API](integration/api/index.md) и [авторизации](integration/api/auth.md).
@@ -28,7 +60,7 @@
- [Bundle cuod](integration/api/bundle-cuod.md) — список 40 справочников ЦУОД
- [Best practices](integration/api/best-practices.md) — частые ошибки
- Я обрабатываю ошибки
- Я обрабатываю ошибки в интеграции
- [Ошибки и rate limits](integration/api/errors.md) — error codes, retry strategy
- [Лучшие практики](integration/api/best-practices.md) — robust integration
@@ -37,11 +69,19 @@
Открой [Swagger UI](https://ordinis.k8s.264.nstart.cloud/swagger-ui.html) —
там можно нажать **Authorize** → войти через Keycloak (PKCE, без secret) →
и сразу делать "Try it out" с реальным JWT. Authorization, error codes,
и сразу делать «Try it out» с реальным JWT. Authorization, error codes,
schemas — всё через interactive UI.
{% endlist %}
## Что нового в продукте
Список значимых обновлений — в [«Что нового»](changelog.md). Обновления
автоматически приходят на твой сервер при следующем deploy.
Кнопка «Что нового» в правом верхнем углу админ-панели показывает то же
содержимое + красная точка с количеством непрочитанных релизов.
## Среды
| Среда | URL | Auth | Назначение |
@@ -54,6 +94,8 @@
- Запросы новых ролей / scope в Keycloak: `zimin.an@nstart.space`
- Bug reports / feature requests: через issue tracker (запросить URL у Ordinis team)
- Проблема с конкретной записью или ошибкой? Сначала загляни в
[Разбор ошибок](operator/errors.md) — часто там сразу ответ
## Версионирование
+95
View File
@@ -0,0 +1,95 @@
# Журнал аудита
Каждое изменение в системе фиксируется в **журнале аудита** — таблице,
в которой записано: кто, что, когда, что было до и после.
Доступ — в разделе **«Аудит»** в навигации (для пользователей с правом
INTERNAL и выше).
## Что попадает в журнал
- Создание, обновление, закрытие записей справочников (`CREATE` / `UPDATE` / `CLOSE`)
- Создание, обновление, удаление справочников
- Schema drafts: создание, отправка на ревью, approve, reject, publish
- Изменения настроек webhook-подписок
Технические события (запуск pod'ов, выполнение SQL миграций) — НЕ
попадают в этот аудит. Они в системных логах.
## Структура записи аудита
| Поле | Что значит |
|---|---|
| **Время** | Когда произошло событие (точное время сервера) |
| **Пользователь** | Кто сделал изменение (берётся из JWT-токена) |
| **Справочник** | Какой справочник затронут |
| **Бизнес-ключ** | Какая конкретно запись (для record-операций) |
| **Действие** | `CREATE`, `UPDATE`, `CLOSE`, `DRAFT_APPROVE` и т.п. |
| **Было / Стало** | Diff: что изменилось (для UPDATE) |
| **Trace ID** | Технический идентификатор для корреляции с логами |
## Фильтрация
В UI — toolbar с фильтрами:
- **Справочник** — конкретный или все
- **Действие** — `CREATE` / `UPDATE` / `CLOSE` и т.д.
- **Пользователь** — кто сделал
- **Бизнес-ключ** — конкретная запись
- **Период** — диапазон дат (используй DateRangePicker)
Применяется автоматически при изменении любого фильтра. URL обновляется —
можно поделиться ссылкой «аудит за вчера на ground_station».
## Что искать в аудите
### «Кто закрыл запись X?»
Фильтр: справочник = `<имя>`, бизнес-ключ = `<код>`, действие = `CLOSE`.
Видишь оператора, время, причину (если была указана).
### «Что менялось вчера в spacecraft?»
Фильтр: справочник = `spacecraft`, период = вчера. Сортировка по времени.
### «Я подозреваю некорректное изменение»
Найди запись, посмотри **diff** колонки «Было / Стало». Если изменение
действительно неверное — раскрой запись, кликни **«Восстановить эту
версию»** (создаст новую активную версию из исторической).
### «Кто часто меняет конкретный справочник?»
Фильтр: справочник + период (последний месяц). В UI отсортируй по
пользователю — видишь топ-операторов.
## Экспорт
Кнопка **«Экспортировать»** в правом верхнем — выгрузка в CSV отфильтрованных
записей. Полезно для:
- Отчётов руководству («что изменилось за квартал»)
- Compliance-проверок
- Анализа в Excel / BI
CSV содержит те же колонки, что в UI, плюс полные diff'ы в JSON-формате.
## Что НЕЛЬЗЯ найти в аудите
- **Чтения** (`READ`) — никто их не записывает (объём огромен, информативная
ценность низкая)
- **Изменения настроек системы** на уровне kubernetes / Vault / SSL —
это в системных логах ops-команды
- **Изменения вне Ordinis** — если внешняя система сделала свою копию
справочника и поменяла её, в нашем аудите этого нет
## Срок хранения
По умолчанию — **бессрочно**. Журнал растёт, но не очищается автоматически.
Если требуется retention policy (например, удалять старше 5 лет по GDPR) —
свяжись с ответственным за систему, это настраивается на уровне БД.
## Trace ID — как использовать
Если в audit-записи есть trace ID — это можно сопоставить с логами
бэкенда. Полезно когда ошибка произошла на стороне сервера и нужно
понять глубинную причину. Скопируй trace ID, отдай в support / ops.
+85
View File
@@ -0,0 +1,85 @@
# Закрытие записи
В Ordinis нет «удалить» в привычном смысле — данные не пропадают. Вместо
этого запись **закрывается**: ей выставляется `valid_to = текущий момент`,
после чего она перестаёт считаться актуальной, но история остаётся.
## Зачем так
Bitemporal модель — два временных измерения:
- **valid_from / valid_to** — когда запись актуальна в реальном мире
- **created_at / updated_at** — когда запись была введена в систему
Это позволяет:
- Восстанавливать что было «на дату» (например, состав группировки
спутников на 2024-12-31)
- Видеть, кто и когда вносил изменения
- Не ломать downstream-системы, которые могут читать архивные данные
## Когда закрывать запись
- Спутник **выведен из эксплуатации**
- Наземная станция **демонтирована**
- Контракт **истёк**
- Тип записи **переименован** (закрыть старую, открыть новую с новым кодом)
- Дубликат **обнаружен** (оставить одну, закрыть остальные)
## Как закрыть
1. Открой запись в drawer'е
2. Кнопка **«Удалить»** в footer'е (она же — «закрыть»)
3. Опционально укажи `valid_to` — момент закрытия. По умолчанию: сейчас.
4. Подтверди
После этого:
- В UI запись по умолчанию **не отображается** (фильтр «только активные»)
- В API при `GET /records?at=YYYY-MM-DD` — отображается только если
на эту дату была активна
- История доступна через `GET /records/{id}/history`
## Как восстановить
Если запись закрыта ошибочно — открой панель **«История версий»** в
drawer'е, найди последнюю активную версию **до закрытия**, нажми
**«Восстановить как новую запись»**. Это создаст новую активную версию
с тем же бизнес-ключом, что и закрытая.
Альтернатива: создать запись с тем же бизнес-ключом заново — система
проверит, что нет пересекающихся периодов действия.
## Bulk-закрытие при cascade
Если запись X **используется** в других справочниках (через `x-references`),
система проверит ссылки:
- Если есть активные записи, ссылающиеся на X — будет предупреждение
- Можно: (а) сначала закрыть/обновить зависимые записи, (б) принять
риск orphan'ов (downstream consumers получат FK-misses)
См. [Cross-references (FK)](../integration/api/x-references.md) о том,
как зависимости валидируются на уровне API.
## Закрытие vs полное удаление
**Полное удаление записи (DELETE из БД) не поддерживается** через UI и
обычный API. Это намеренно — целостность истории важнее «чистоты».
Если ОЧЕНЬ нужно (например, по GDPR / по приказу) — администратор
выполняет это через специальную процедуру с подтверждением. Свяжись с
ответственным за систему.
## Что произойдёт при закрытии
- В БД у записи обновляется `valid_to`
- В Kafka-топик публикуется событие `RecordClosed` с указанием
`valid_to` и причины (если задана)
- В audit log пишется операция `CLOSE` с привязкой к оператору
- Downstream consumers (Альтум, BI) видят новую версию состояния
справочника на следующем чтении (или сразу через событие)
## Чек-лист перед закрытием
- [ ] Я уверен, что закрываю правильную запись (бизнес-ключ совпадает)
- [ ] Если у записи есть зависимости через FK — я их проверил
- [ ] `valid_to` соответствует реальному моменту вывода из эксплуатации
(не «сейчас» по умолчанию, если эксплуатация закончилась в прошлом)
- [ ] Я понимаю: история останется, downstream-консьюмеры узнают
+118
View File
@@ -0,0 +1,118 @@
# Создание справочника
Справочник — это таблица однородных записей с фиксированной схемой полей.
Прежде чем создавать его в системе, продумай: какие поля будут, какие
обязательны, какие ссылаются на другие справочники.
## Когда нужен новый справочник
- Нужны однородные данные (>5 записей со схожей структурой)
- Записи будут переиспользоваться: на них будут ссылаться другие
справочники или внешние системы
- Структура данных стабильна (поля меняются редко)
Если просто нужна пара значений «для себя» — лучше добавь поле-перечисление
(enum) в существующий справочник, чем заводи отдельный.
## Шаги
### 1. Открой каталог
В админ-панели — раздел «Справочники». Кнопка **«Создать справочник»** в
правом верхнем углу.
### 2. Заполни основные поля
- **Имя (name)** — машинное имя, латиница в snake_case (`spacecraft`,
`ground_station`). По этому имени справочник будет доступен в API
и других справочниках через FK.
- **Отображаемое имя** — человеческое, как видит оператор
(«Космические аппараты», «Наземные станции»).
- **Версия схемы** — `1.0.0`, повышается при изменении схемы (см.
[schema drafts](../reviewer/schema-drafts.md)).
- **Bundle** — к какому набору справочников относится (`cuod` по умолчанию).
- **Scope** — `PUBLIC` / `INTERNAL` / `RESTRICTED`. Кто сможет читать.
### 3. Опиши схему полей
Схема справочника — это **JSON Schema** (стандарт описания структуры
JSON-объектов). В редакторе (Monaco) задаёшь свойства записи.
Минимальный пример:
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"x-id-source": "code",
"required": ["code"],
"properties": {
"code": {
"type": "string",
"x-unique": true,
"description": "Уникальный код"
},
"name": {
"type": "object",
"x-localized": true
}
}
}
```
#### Полезные аннотации
| Аннотация | Что делает |
|---|---|
| `x-id-source: "code"` | Бизнес-ключ записи берётся из поля `code` (а не задаётся отдельно) |
| `x-unique: true` | Значение уникально по справочнику (например, нет двух записей с одинаковым `code`) |
| `x-localized: true` | Поле локализованное — хранит значения для каждого языка (`{ru: "Земля", en: "Earth"}`) |
| `x-references: "dict.field"` | Внешний ключ — ссылка на запись другого справочника |
| `description: "..."` | Подсказка для оператора, показывается в UI рядом с полем |
| `enum: ["A", "B"]` | Список допустимых значений |
| `format: "date"` / `"date-time"` | Календарный picker в форме |
#### Пример со ссылкой на другой справочник
```json
{
"type": "string",
"x-references": "satellite_type.code",
"description": "Тип спутника"
}
```
При вводе оператор увидит выпадающий список типов из справочника
`satellite_type`.
### 4. Опубликуй
После нажатия **«Создать»** справочник появляется в каталоге, схема
блокируется (запись валидируется по ней). Чтобы изменить схему позже —
создай [schema draft](../reviewer/schema-drafts.md) и пройди ревью.
## Что произойдёт автоматически
При создании справочника:
- В БД появляется таблица записей с этой схемой
- В REST API endpoint становится доступен: `/api/v1/dictionaries/<name>/records`
- Kafka-топик начинает получать события об изменениях записей
- Audit log начинает писать историю операций
## Чек-лист перед созданием
- [ ] Машинное имя в snake_case
- [ ] Все обязательные поля помечены в `required`
- [ ] Бизнес-ключ задан (`x-id-source` или поле в `required`)
- [ ] Локализованные поля имеют `x-localized: true`
- [ ] FK-поля указывают на существующие справочники
- [ ] Описания (`description`) даны для каждого поля — это твоя памятка
и помощь следующему оператору
## Что НЕ стоит делать
- ❌ Создавать справочник «на всякий случай» без планируемых записей
- ❌ Класть бизнес-логику в схему (валидации вида «если X то Y») — это
приведёт к жёсткой связке, тяжело менять
- ❌ Зашивать константы в `description` — описание читается оператором,
но валидацию по нему не построишь
+101
View File
@@ -0,0 +1,101 @@
# Работа с записями
Запись — одна строка в справочнике (например, конкретный космический
аппарат «ISS» в справочнике `spacecraft`).
## Добавить запись
1. Открой справочник в каталоге
2. Кнопка **«Создать запись»** в правом верхнем
3. Заполни поля по схеме — обязательные помечены звёздочкой
4. **Период действия**`valid_from` / `valid_to`:
- `valid_from` — с какой даты запись актуальна (по умолчанию: сейчас)
- `valid_to` — до какой даты (по умолчанию: открыто)
5. **«Сохранить»** — запись появляется в списке
### Бизнес-ключ
Если у справочника `x-id-source: "code"`, бизнес-ключ берётся из поля
`code`. Иначе — задаётся отдельно при создании. Бизнес-ключ должен быть
уникален: попытка создать запись с уже существующим ключом → ошибка 409.
### Локализованные поля
Поля с `x-localized: true` имеют отдельные значения для каждого языка:
```
Название [ru]: Международная космическая станция
Название [en]: International Space Station
```
Если оператор ввёл только одну локаль — остальные пусты, при чтении в
другой локали покажется fallback на язык по умолчанию словаря (обычно
`ru` для ЦУОД).
### FK-поля (ссылки)
Поля с `x-references` отображаются как выпадающий список. Например, у
`spacecraft` поле `type` ссылается на `satellite_type.code` — оператор
выбирает значение из готового списка типов. Если нужный тип отсутствует —
сначала добавь его в справочник `satellite_type`, потом возвращайся.
## Редактировать запись
1. Клик по записи в списке → открывается боковая панель (drawer)
2. Меняешь нужные поля, **«Сохранить»**
3. Новая версия записи создаётся; **предыдущая остаётся в истории**
(см. [bitemporal модель](../integration/api/bitemporal.md))
История версий доступна:
- В drawer'е — кнопка **«История версий»**
- В timeline-режиме (`TimeTravel`) — слайдер по датам
### Что нельзя редактировать
- **Бизнес-ключ** — фиксирован с момента создания. Если ошибся — нужно
закрыть запись (см. [Закрытие записи](close-record.md)) и создать новую.
- **Схему** — общая для всех записей справочника, меняется только через
[schema draft](../reviewer/schema-drafts.md).
## Поиск и фильтрация
В каталоге справочника — toolbar поиска и фильтров:
- **Поиск по тексту** — ищет по бизнес-ключу и локализованным полям
- **Только заполненные** — toggle, скрывает записи где конкретное поле пустое
- **Период** — фильтр по `valid_from` / `valid_to`
- **Scope** — фильтр по уровню доступа (для администраторов)
### Глобальный поиск
В шапке (TopBar) — `⌘K` / `Ctrl+K` — поиск через все справочники по
бизнес-ключу или фрагменту названия.
## Bulk-операции
Сейчас bulk import / export — **только через API** ([интеграционный гайд](../integration/api/index.md)).
В UI массовых операций пока нет — добавятся в будущих версиях по запросам
от операторов.
## Что произойдёт при сохранении
- В БД добавляется новая версия записи
- В Kafka-топик публикуется событие `RecordCreated` или `RecordUpdated`
(потребители — Альтум, геопортал, BI — узнают об изменении в течение
секунд)
- В audit log пишется кто изменил, когда, что было до и после
- Если справочник имеет включённую Redis-проекцию — кэш обновляется
автоматически
## Типичные ошибки
См. [Разбор ошибок](errors.md). Самые частые:
- **«Бизнес-ключ уже существует»** (409) — попробуй другой `code` или
проверь, не закрыта ли существующая запись (фильтр истории)
- **«Поле X обязательно»** (422) — посмотри `required` в схеме
- **«FK не найден»** (422) — значение `x-references` указывает на запись,
которой нет; добавь в referenced справочник
- **«Период действия пересекается»** (409) — для bitemporal: нельзя
иметь две активные версии одной записи в один момент. См.
[Закрытие записи](close-record.md).
+144
View File
@@ -0,0 +1,144 @@
# Разбор ошибок валидации
Ordinis старается давать понятные ошибки, но иногда нужна расшифровка.
Эта страница — справочник самых частых.
## Формат ошибок
В UI ошибки появляются:
- **Инлайн под полем формы** — для конкретных полей (red text)
- **Тост в правом верхнем** — для общих ошибок (например, сеть)
- **Полноэкранный экран** — для серьёзных проблем (500 / 503)
Каждая ошибка имеет:
- **Код** (`webhook_status_invalid`, `record_not_active`) — машинный идентификатор
- **Сообщение** — человеческое описание на русском
- **Trace ID** (для серверных ошибок) — для передачи в ops
## Самые частые ошибки
### 422: «Поле X обязательно»
Что значит: схема справочника требует это поле, но ты его не заполнил.
Что делать: заполни и сохрани. Если поле кажется ненужным — это вопрос
к схеме справочника, обсуди с ревьюером (см. [schema drafts](../reviewer/schema-drafts.md)).
### 422: «Тип значения не соответствует схеме»
Что значит: например, в поле `mass_kg` (number) введён текст.
Что делать: проверь тип в hint'е под полем. Числовые поля принимают только
цифры (точка как разделитель десятых для дробей: `1234.5`).
### 422: «FK не найден»
Что значит: значение в поле со ссылкой (`x-references`) указывает на
запись, которой нет в целевом справочнике.
Что делать:
1. Открой целевой справочник (имя есть в hint'е поля)
2. Проверь, есть ли там запись с этим бизнес-ключом
3. Если нет — сначала добавь запись туда, потом возвращайся
4. Если есть, но закрыта — нужно её **активировать** или создать новую
### 409: «Бизнес-ключ уже существует»
Что значит: ты пытаешься создать запись с `code`, который уже занят
активной записью того же справочника.
Что делать:
- Выбери другой `code`
- Или: найди существующую запись и обнови её, а не создавай новую
- Или: закрой существующую (если она устарела) и создай заново
### 409: «Период действия пересекается»
Что значит: bitemporal модель не разрешает двум версиям одной записи
быть активными одновременно.
Что делать:
1. Найди активную версию того же бизнес-ключа
2. **Закрой её** (`valid_to = момент новой версии`), потом создавай новую
3. Или: укажи `valid_from` новой версии так, чтобы периоды не пересекались
См. [Закрытие записи](close-record.md).
### 403: «Нет доступа»
Что значит: у твоего пользователя нет прав на эту операцию или scope.
Что делать:
- Если справочник `RESTRICTED` — нужны права администратора, спроси у
руководителя
- Если `INTERNAL` — нужен внутренний токен (не публичный)
- Если операция «approve schema draft» — это право ревьюера, не оператора
(см. [reviewer guide](../reviewer/index.md))
### 401: «Требуется вход»
Что значит: твоя сессия истекла или нет валидного токена.
Что делать: войди заново через Keycloak. После logout / refresh всё должно
работать.
### 503: «Сервер временно недоступен»
Что значит: backend временно не отвечает (rolling restart, плановое
обслуживание, или сетевой сбой).
Что делать:
1. Подожди 30-60 секунд, нажми **«Повторить»**
2. Если повторяется > 5 минут — свяжись с ops
3. Никакие данные не пропали — сервер вернётся, твои изменения сохранены
локально пока не отправил
### Network error / «Нет соединения»
Что значит: твой браузер не может достучаться до сервера. Чаще всего
проблема в твоей сети, не в Ordinis.
Что делать:
1. Проверь соединение (открой любой другой сайт)
2. Если корпоративная сеть — проверь VPN / прокси
3. Если изменения не сохранились — они **не пропадут**, попробуй
повторить когда соединение вернётся
### «AxiosError: Request failed with status code 503» (raw)
Что значит: техническая ошибка, которая «убежала» в UI без friendly
обёртки. Это **баг UI**, не баг данных.
Что делать: сделай скриншот, передай в support с trace ID (если есть).
## Schema validation на стороне сервера
Когда ты сохраняешь запись, сервер валидирует её по JSON Schema справочника.
Если ошибка — увидишь подробности:
```
Validation failed:
- /satellite_type: must be one of ['OPERATIONAL', 'TESTING', 'DECOMMISSIONED']
- /mass_kg: must be number, got string
- /name: required property missing
```
Каждая строка — отдельная проблема. Поправь все, сохрани заново.
## Если не помогает
1. **Скриншот** — снимок экрана со всеми видимыми деталями ошибки
2. **Trace ID** — если есть в сообщении, скопируй
3. **Время** — точное время, когда возникла
4. **Что ты делал** — последняя последовательность действий
5. Передай это команде поддержки. По этим данным они смогут восстановить
контекст через [журнал аудита](audit-log.md) и серверные логи.
## Что НЕ нужно делать
-**Очищать кэш браузера / cookies** — это сломает сессию, ошибку не уберёт
-**Открывать в инкогнито** — то же самое
-**Менять JSON вручную в DevTools** — данные не сохранятся всё равно,
но можешь напугать себя
-**Заводить дубликат записи «чтобы попробовать»** — если первая не
сохранилась, она и не сохранилась, дубликат не появится
+53
View File
@@ -0,0 +1,53 @@
# Руководство оператора Ordinis НСИ
Это руководство — для **операторов справочников**: тех, кто заводит и
поддерживает данные в Ordinis (космические аппараты, наземные станции,
типы антенн, операторов спутниковой связи и т.д.).
Если ты **разработчик-интегратор** (Альтум, BI, геопортал) — смотри
[интеграцию по API](../integration/api/index.md).
Если ты **ревьюер** schema drafts — смотри
[руководство ревьюера](../reviewer/index.md).
## Что внутри
- [Создание справочника](create-dictionary.md) — как добавить новый
справочник (например, «типы антенн») и описать его поля
- [Работа с записями](edit-record.md) — как добавлять, редактировать,
искать записи внутри справочника
- [Закрытие записи](close-record.md) — как корректно «удалить» запись,
сохранив историю (bitemporal модель)
- [Журнал аудита](audit-log.md) — как посмотреть, кто и когда что менял
- [Разбор ошибок](errors.md) — типичные ошибки валидации и как их понять
## Базовые понятия
**Справочник** — таблица с однородными записями (например, «спутники»,
«наземные станции», «частотные диапазоны»). У каждого справочника есть
**схема** — описание полей: какие поля, какого типа, обязательные или
нет, ссылаются ли на другие справочники.
**Запись** — одна строка в справочнике. У каждой записи есть **бизнес-ключ**
(уникальный код) и **данные** (значения полей по схеме). Также есть
**период действия**`valid_from` / `valid_to`, в течение которого
запись считается актуальной.
**Scope** — уровень доступа к данным:
- `PUBLIC` — открыто всем
- `INTERNAL` — для внутренних потребителей
- `RESTRICTED` — только для администраторов
**Schema draft** — предложение изменить схему справочника (добавить поле,
изменить тип). Не применяется автоматически — проходит ревью, затем
публикуется.
## Какую роль ты выполняешь
| Если ты... | Иди в раздел |
|---|---|
| Создаёшь новый справочник | [Создание справочника](create-dictionary.md) |
| Добавляешь запись в существующий справочник | [Работа с записями](edit-record.md) |
| Хочешь «удалить» устаревшую запись | [Закрытие записи](close-record.md) |
| Видишь ошибку валидации, не понимаешь | [Разбор ошибок](errors.md) |
| Проверяешь, кто менял запись | [Журнал аудита](audit-log.md) |
+70
View File
@@ -0,0 +1,70 @@
# Руководство ревьюера Ordinis
Это руководство — для **ревьюеров schema drafts**: тех, кто принимает или
отклоняет предложения изменить схему справочников.
Если ты **оператор** (заводишь записи) — смотри
[руководство оператора](../operator/index.md).
## Когда нужен ревью
Изменения схемы справочника (добавление поля, изменение типа, новое
ограничение) **не применяются автоматически**. Оператор создаёт **schema
draft** — предложение, которое проходит через ревью:
```
DRAFT → REVIEW_PENDING → APPROVED → PUBLISHED
↘ REJECTED ↘ CHANGES_REQUESTED
↘ WITHDRAWN
```
Ты как ревьюер решаешь: безопасно ли применить это изменение? Не сломает
ли оно существующих записей? Не сломает ли консьюмеров (Альтум, BI и пр.)?
## Что внутри
- [Ревью schema drafts](schema-drafts.md) — пошаговая инструкция, как
смотреть diff, какие проверки делать, как принимать решение
## Когда чаще всего стоит REJECT или REQUEST_CHANGES
| Сценарий | Что делать |
|---|---|
| Меняется тип существующего поля (string → number) | **REJECT** — поломает существующие записи. Если очень нужно, делать через миграцию данных, не через draft |
| Удаляется обязательное поле | **REJECT** или **REQUEST_CHANGES** с альтернативой (мигрировать данные) |
| Добавляется обязательное поле без default | **REQUEST_CHANGES** — нужен default или поле должно быть опциональным |
| Добавляется FK без проверки целостности | **REQUEST_CHANGES** — попроси добавить orphan check |
| Меняется `enum` (удаляется значение) | **REQUEST_CHANGES** — закрой все записи с этим значением сначала |
| Просто добавляется опциональное поле | Обычно **APPROVE** — безопасно |
| Косметика (description, examples) | **APPROVE** — это документация |
## Когда APPROVE
- Добавление опционального поля
- Расширение `enum` (новое значение)
- Уточнение `description` / `examples`
- Добавление формата валидации (`format: date`) к ранее не-валидируемому полю,
если все существующие данные ему соответствуют
## Базовые принципы
1. **Не ломай существующие записи.** Если изменение делает 1+ запись
невалидной — это breaking change, нужна data migration.
2. **Не ломай downstream.** Если поле читают Альтум / геопортал / BI — спроси
их перед изменением имени или типа.
3. **Документируй причину.** Approve / reject комментарий — это аудит.
Через год кто-то будет смотреть «почему мы так сделали».
4. **Сомневаешься — REQUEST_CHANGES.** Это не отказ, это просьба автора
уточнить. Лучше потратить день на обсуждение, чем неделю на rollback.
## Связанные материалы
- [Обзор API](../integration/api/index.md) — чтобы понять, как изменение
отразится на потребителях
- [Bitemporal модель](../integration/api/bitemporal.md) — как работают
периоды действия (важно для миграций)
- [Cross-references](../integration/api/x-references.md) — как изменения
FK влияют на зависимые справочники
+243
View File
@@ -0,0 +1,243 @@
# Ревью schema drafts
Пошаговая инструкция: как смотреть драфт, какие проверки делать, как
принимать решение.
## Где найти драфты на ревью
В навигации — раздел **«Согласования»** (`/reviews`). Вкладка **«Схемы»** —
ожидающие schema drafts (отсортированы по дате создания, старые сверху).
Также: на странице справочника, если у него **активный draft**, появляется
жёлтый банер «На согласовании: <автор>». Клик → откроется drawer с
содержимым драфта.
## Окно ревью (Schema Draft Drawer)
При открытии драфта видишь:
```
┌──────────────────────────────────────────────────────────────┐
│ Согласование draft [×] │
│ автор: zimin.an · отправлен: 2026-05-12 14:00 · v1.0.0→2.0.0│
├──────────────────────────────────────────────────────────────┤
│ Комментарий автора: │
│ «Добавляем поле frequency_band — будет FK на │
│ frequency_band.code, пока опциональное чтобы не ломать │
│ существующие записи.» │
├──────────────────────────────────────────────────────────────┤
│ Текущая (live) schema │ Предложено │
│ { │ { │
│ "code": ... │ "code": ..., │
│ "name": ... │ "name": ..., │
│ } │ "frequency_band": { │
│ │ "x-references": │
│ │ "frequency_band.code" │
│ │ } │
│ │ } │
├──────────────────────────────────────────────────────────────┤
│ ◯ Breaking change keys: нет │
│ ✓ Все существующие записи остаются валидными │
├──────────────────────────────────────────────────────────────┤
│ Комментарий ревьюера: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ... │ │
│ └──────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ [REJECT] [REQUEST CHANGES] [APPROVE] [PUBLISH] │
└──────────────────────────────────────────────────────────────┘
```
## Что проверять — чек-лист
### 1. Понимаешь ли ты, ЧТО автор хочет сделать?
Прочти комментарий автора. Он должен объяснить:
- **Зачем** менять схему (новое требование? bug fix?)
- **Что** меняется (поле, тип, ограничение)
- **Что с существующими записями** (мигрируем? остаются как есть?)
Если непонятно — **REQUEST CHANGES** с вопросом. Не угадывай намерения.
### 2. Diff schema — что изменилось
В drawer'е — side-by-side сравнение. Смотри:
- **Новые поля** — выделены зелёным
- **Удалённые поля** — красным
- **Изменённые поля** — жёлтым (с подсветкой, что именно)
- **Breaking change keys** — система автоматически помечает: `type`,
`format`, `required`, `x-references`, `*Of`, удаления
### 3. Совместимость с существующими записями
Откладывается всё на server: при попытке `publish` сервер прогоняет все
существующие записи через новую схему и проверяет.
Но ревьюер должен **проверить руками** в простых случаях:
- Поле добавляется как **обязательное** (`required`) без default? → существующие
записи становятся невалидными
- Поле удаляется и оно **использовалось**? → потеряется данные
- Меняется `enum` (убирается значение)? → записи с этим значением станут
невалидны
Если сомневаешься — **запроси у автора**: «прогони `GET /records?validate=new-schema`
и пришли результат». Эндпоинт покажет, сколько записей сломается.
### 4. Влияние на downstream
Schema change часто требует **уведомления потребителей**:
- Альтум — если меняем поле, которое они используют в карточках
- Геопортал — если меняем geo-поля
- BI — если меняем имена / типы полей, которые ушли в их витрины
Если не уверен — **REQUEST CHANGES** с просьбой согласовать с командами.
Лучше потратить 2 дня на согласование, чем 2 дня на rollback.
### 5. Версионирование
Схема имеет `schemaVersion` (`1.0.0`, `2.0.0`). Правила:
- **Major bump** — breaking change (удаление, изменение типа,
обязательность). Все консьюмеры должны быть готовы.
- **Minor bump** — backward-compatible (добавление опциональных полей,
расширение `enum`, новые `description`).
- **Patch** — косметика (документация, examples).
Если автор предлагает minor bump для breaking change — **REQUEST CHANGES**
с просьбой обновить версию.
### 6. Тесты / валидация
Если справочник имеет sample data в bundle — после schema change нужно
проверить, что sample data всё ещё валидна. Это проверяет CI автоматически
после publish, но лучше catch'нуть раньше.
## Действия
### APPROVE
Изменение безопасное, можно публиковать. Кнопка переводит статус драфта в
`APPROVED`. **Публикация ещё не произошла** — нужен отдельный клик
«PUBLISH», который применяет схему к live справочнику.
Опциональный комментарий — почему approve. Появится в audit log.
### REJECT
Изменение неприемлемо в принципе, обсуждать нечего. Например:
- Удаляется поле, которое critical для бизнеса
- Меняется тип так, что данные потеряются необратимо
Драфт переходит в `REJECTED`. **Обязателен комментарий** — почему отказ.
Автор увидит, может создать новый драфт с учётом feedback.
### REQUEST CHANGES
Изменение в принципе ОК, но требует доработки. Например:
- Нужно изменить версию (major вместо minor)
- Нужно сделать поле опциональным с default
- Нужно добавить data migration script
Драфт переходит в `CHANGES_REQUESTED`. Автор увидит, отредактирует, отправит
заново (вернётся в `REVIEW_PENDING`). **Обязателен комментарий** — что
именно править.
### WITHDRAW
Только автор может withdraw свой драфт (если передумал). Ревьюер этого не
делает.
### PUBLISH
Только после `APPROVE`. Применяет новую схему к live справочнику:
- Обновляется `schema_definition` в БД
- Все будущие записи валидируются по новой схеме
- Существующие записи **не модифицируются** (их данные остаются как есть)
- Публикуется событие в Kafka — консьюмеры узнают о смене схемы
После publish откатить **нельзя в один клик** — нужен новый draft с
inverse change.
## Что произойдёт при APPROVE → PUBLISH
1. `dictionary_definitions.schema_json` обновляется до новой версии
2. `dictionary_definitions.schema_version` инкрементируется
3. В audit log пишется операция `SCHEMA_PUBLISHED`
4. В Kafka топик публикуется `DictionarySchemaUpdated` event с before/after
5. Redis projection (если включена) cascade-invalidates dependent projections
6. Frontend admin-ui получает push notification (через next polling) →
баннер «новая схема» появляется у активных операторов
## Типичные паттерны
### Добавление опционального поля
```
- frequency_band: {x-references: "frequency_band.code"} ← новое, опциональное
```
Обычно **APPROVE**. Существующие записи остаются валидными (поле просто
`undefined`).
### Добавление обязательного поля без default
```
- mass_kg: {type: "number"} ← новое, в required
```
Обычно **REQUEST CHANGES** — либо `default`, либо `nullable`, либо
сначала миграция данных.
### Изменение `x-references`
```
- type: {x-references: "satellite_type.code"}
+ type: {x-references: "satellite_type_v2.name"}
```
**REQUEST CHANGES** в подавляющем большинстве случаев. Если справочник
переименован — нужно migration data, не просто schema swap.
### Расширение `enum`
```
- status: {enum: ["ACTIVE", "INACTIVE"]}
+ status: {enum: ["ACTIVE", "INACTIVE", "MAINTENANCE"]}
```
**APPROVE** — обратно совместимо.
### Сужение `enum`
```
- status: {enum: ["ACTIVE", "INACTIVE", "MAINTENANCE"]}
+ status: {enum: ["ACTIVE", "INACTIVE"]}
```
**REJECT** или **REQUEST CHANGES** — записи со значением `MAINTENANCE`
станут невалидны. Нужна data migration сначала.
### Удаление поля
```
- legacy_id: {type: "string", x-unique: true}
```
Опасно. Спроси:
- Кто-то использует это поле в downstream?
- Что мы делаем с данными в нём?
- Не лучше ли его deprecate (оставить, не использовать)?
Обычно **REQUEST CHANGES** с обсуждением.
## Если ошибся
Ревьюеры — люди. Если approve'нул и потом понял что зря:
1. **Если PUBLISH ещё не произошёл** — попроси автора `WITHDRAW`, потом
`REJECT` или создать новый
2. **Если PUBLISH произошёл** — создай **новый schema draft** с inverse
change (откати изменение), пройди ревью заново
3. **Документируй в audit** — комментарий «откатываю, потому что X» —
важная информация для будущих ревьюеров
Это не катастрофа, но и не норма. Если ошибаешься > 1 раза в квартал —
стоит пересмотреть процедуру ревью (может, нужен mandatory peer-review).
+26 -1
View File
@@ -1,10 +1,35 @@
title: Ordinis НСИ ЦУОД ОДХ — Integrator Guide
title: Ordinis НСИ ЦУОД ОДХ
href: index.md
items:
- name: Введение
href: index.md
- name: Что нового
href: changelog.md
- name: Руководство оператора
items:
- name: Обзор
href: operator/index.md
- name: Создание справочника
href: operator/create-dictionary.md
- name: Работа с записями
href: operator/edit-record.md
- name: Закрытие записи
href: operator/close-record.md
- name: Журнал аудита
href: operator/audit-log.md
- name: Разбор ошибок
href: operator/errors.md
- name: Руководство ревьюера
items:
- name: Обзор
href: reviewer/index.md
- name: Ревью schema drafts
href: reviewer/schema-drafts.md
- name: Интеграция по API
items:
- name: Обзор
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
import { Link, useLocation } from '@tanstack/react-router'
import { useDictionaries, useDictionaryDetail } from '@/api/queries'
import { VersionBadge } from '@/components/version/VersionBadge'
import { WhatsNewButton } from '@/components/version/WhatsNewButton'
import { Bell, Menu } from 'lucide-react'
/**
@@ -92,6 +93,7 @@ export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
<span className="hidden md:inline-flex">
<VersionBadge />
</span>
<WhatsNewButton />
<NotificationsBell />
</div>
</header>
@@ -103,6 +105,9 @@ export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
* (webhook errors, draft reviews, system notices). Click → drawer/popup.
* Сейчас static bell без counter — wire'ится когда backend notifications
* stream появится.
*
* <p>«Что нового» (WhatsNewButton) — adjacent button показывает release
* notes, отделён от operational notifications (разные mental models).
*/
function NotificationsBell() {
const { t } = useTranslation()
@@ -0,0 +1,207 @@
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { GiftIcon, SparkleIcon } from '@phosphor-icons/react'
import { Modal, Badge } from '@/ui'
import {
useChangelog,
readLastSeenVersion,
writeLastSeenVersion,
isVersionNewer,
type ChangelogEntry,
type ChangelogSection,
} from './useChangelog'
import { cn } from '@/lib/utils'
/**
* «Что нового» — button в TopBar + modal с release notes.
*
* <p>Behavior:
* <ul>
* <li>Button показывает sparkle icon + red dot если есть unread versions
* (entries newer than {@code localStorage.ord-whatsnew-last-seen}).</li>
* <li>Click → modal с list of versions, newest first, max 10 recent.</li>
* <li>Modal open → mark latest version как seen (persisted в localStorage).</li>
* <li>Дальнейшие visits: dot не показывается пока новый release tag не shipped.</li>
* </ul>
*
* <p>Source: CHANGELOG.md bundled через Vite {@code ?raw} import → parsed
* клиент-side. No backend changes.
*
* <p>i18n: button + modal title. Entries themselves остаются как-они есть из
* CHANGELOG (mix ru/en commit messages — это intentional, авторы commit'ов
* вписывают на родном языке).
*/
export function WhatsNewButton() {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const entries = useChangelog()
// useState вместо useUnreadCount чтобы re-compute после modal close
// (localStorage write не trigger'ит React re-render).
const [lastSeen, setLastSeen] = useState<string | null>(() => readLastSeenVersion())
const unreadCount = useMemo(
() => entries.filter((e) => isVersionNewer(e.version, lastSeen)).length,
[entries, lastSeen],
)
// На modal close — persist latest version как seen.
useEffect(() => {
if (open && entries.length > 0) {
const latest = entries[0].version
writeLastSeenVersion(latest)
setLastSeen(latest)
}
}, [open, entries])
// Show только 10 recent entries — older releases via «full changelog» link
const recent = entries.slice(0, 10)
const label = t('whatsnew.button', { defaultValue: 'Что нового' })
return (
<>
<button
type="button"
title={label}
aria-label={label}
onClick={() => setOpen(true)}
className={cn(
'relative size-7 rounded-md text-ink-2 hover:text-ink hover:bg-surface-2',
'inline-flex items-center justify-center transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
)}
>
<GiftIcon size={14} weight={unreadCount > 0 ? 'fill' : 'regular'} />
{unreadCount > 0 && (
<span
aria-label={t('whatsnew.unread', {
count: unreadCount,
defaultValue: `${unreadCount} новых`,
})}
className="absolute -top-0.5 -right-0.5 inline-flex items-center justify-center min-w-[14px] h-3.5 px-1 rounded-full bg-mars text-on-mars text-[9px] font-semibold leading-none"
>
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
</button>
<Modal
isOpen={open}
onClose={() => setOpen(false)}
title={
<span className="inline-flex items-center gap-2">
<SparkleIcon size={18} weight="fill" className="text-accent" />
{t('whatsnew.title', { defaultValue: 'Что нового в Ordinis' })}
</span>
}
description={t('whatsnew.description', {
defaultValue:
'Последние релизы продукта. Обновления автоматически попадают на ваш сервер при следующем deploy.',
})}
maxWidth="max-w-2xl"
>
<div className="max-h-[60vh] overflow-y-auto -mx-1 px-1">
{recent.length === 0 ? (
<div className="text-mute text-body py-8 text-center">
{t('whatsnew.empty', {
defaultValue: 'Список релизов пока пуст.',
})}
</div>
) : (
<ul className="space-y-5">
{recent.map((entry) => (
<li key={entry.version}>
<EntryCard
entry={entry}
isUnread={isVersionNewer(entry.version, lastSeen)}
/>
</li>
))}
</ul>
)}
</div>
<div className="mt-4 pt-3 border-t border-line-2 text-cap text-mute text-center">
{t('whatsnew.footer', {
defaultValue: 'Полный список изменений — см. CHANGELOG.md в репозитории.',
})}
</div>
</Modal>
</>
)
}
function EntryCard({ entry, isUnread }: { entry: ChangelogEntry; isUnread: boolean }) {
const { t } = useTranslation()
return (
<div
className={cn(
'rounded-lg border p-4 transition-colors',
isUnread
? 'border-accent/40 bg-accent-bg/40'
: 'border-line bg-surface',
)}
>
<div className="flex items-baseline gap-2 mb-3">
<span className="text-title-sm text-ink font-semibold tabular-nums">
v{entry.version}
</span>
{entry.date && (
<span className="text-cap text-mute tabular-nums">{entry.date}</span>
)}
{isUnread && (
<Badge variant="info" className="ml-auto">
{t('whatsnew.newBadge', { defaultValue: 'новое' })}
</Badge>
)}
</div>
{entry.sections.length === 0 ? (
<div className="text-cell text-mute italic">
{t('whatsnew.entryEmpty', {
defaultValue: 'Описание не указано.',
})}
</div>
) : (
<div className="space-y-3">
{entry.sections.map((section, i) => (
<SectionBlock key={i} section={section} />
))}
</div>
)}
</div>
)
}
function SectionBlock({ section }: { section: ChangelogSection }) {
return (
<div>
<h4 className="text-cap text-mute font-semibold mb-1.5 inline-flex items-center gap-1.5">
{section.emoji && <span aria-hidden>{section.emoji}</span>}
<span>{section.title}</span>
</h4>
<ul className="space-y-1 ml-1">
{section.items.map((item, i) => (
<li key={i} className="text-body text-ink-2 flex items-start gap-1.5">
<span className="text-mute mt-1 shrink-0" aria-hidden></span>
<span className="min-w-0 flex-1">
{item.scope && (
<span className="text-mono text-cap text-accent mr-1.5">{item.scope}:</span>
)}
<span>{item.description}</span>
{item.commit && item.commitUrl && (
<a
href={item.commitUrl}
target="_blank"
rel="noreferrer noopener"
className="text-mono text-cap text-mute hover:text-ink ml-1.5"
title={`Commit ${item.commit}`}
>
{item.commit.substring(0, 7)}
</a>
)}
</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,133 @@
import { describe, it, expect } from 'vitest'
import { parseChangelog, isVersionNewer } from './useChangelog'
describe('parseChangelog', () => {
it('parses single version с features section', () => {
const raw = `# Release Notes
## [2.14.0](https://example.com/compare/v2.13.0...v2.14.0) (2026-05-12)
### ✨ Features
* **webhook:** time-series histogram stats endpoint ([5479142](https://example.com/commit/5479142))
`
const entries = parseChangelog(raw)
expect(entries).toHaveLength(1)
expect(entries[0].version).toBe('2.14.0')
expect(entries[0].date).toBe('2026-05-12')
expect(entries[0].compareUrl).toContain('compare/v2.13.0...v2.14.0')
expect(entries[0].sections).toHaveLength(1)
expect(entries[0].sections[0].emoji).toBe('✨')
expect(entries[0].sections[0].title).toBe('Features')
expect(entries[0].sections[0].items).toHaveLength(1)
expect(entries[0].sections[0].items[0].scope).toBe('webhook')
expect(entries[0].sections[0].items[0].description).toBe('time-series histogram stats endpoint')
expect(entries[0].sections[0].items[0].commit).toBe('5479142')
expect(entries[0].sections[0].items[0].commitUrl).toContain('/commit/5479142')
})
it('parses multiple versions newest-first', () => {
const raw = `## [2.14.0](url1) (2026-05-12)
### ✨ Features
* **a:** first ([abc1234](url2))
## [2.13.0](url3) (2026-05-11)
### 🐛 Fixes
* **b:** second ([def5678](url4))
`
const entries = parseChangelog(raw)
expect(entries.map((e) => e.version)).toEqual(['2.14.0', '2.13.0'])
})
it('handles section без emoji', () => {
const raw = `## [1.0.0](url) (2026-01-01)
### Features
* first feature
`
const entries = parseChangelog(raw)
expect(entries[0].sections[0].emoji).toBeUndefined()
expect(entries[0].sections[0].title).toBe('Features')
})
it('handles item без scope', () => {
const raw = `## [1.0.0](url) (2026-01-01)
### Features
* description without scope
* another one ([abc1234](url2))
`
const entries = parseChangelog(raw)
const items = entries[0].sections[0].items
expect(items[0].scope).toBeUndefined()
expect(items[0].description).toBe('description without scope')
expect(items[1].scope).toBeUndefined()
expect(items[1].description).toBe('another one')
expect(items[1].commit).toBe('abc1234')
})
it('handles empty changelog', () => {
expect(parseChangelog('')).toEqual([])
expect(parseChangelog('# Just a title\n\nNo versions yet.')).toEqual([])
})
it('handles version без date', () => {
const raw = `## [1.0.0](url)
### Features
* foo
`
const entries = parseChangelog(raw)
expect(entries[0].version).toBe('1.0.0')
expect(entries[0].date).toBeUndefined()
})
it('handles version с multiple sections', () => {
const raw = `## [2.10.0](url) (2026-05-12)
### ✨ Features
* **timetravel:** new tab
### 🐛 Fixes
* **ux:** friendly errors
`
const entries = parseChangelog(raw)
expect(entries[0].sections).toHaveLength(2)
expect(entries[0].sections[0].title).toBe('Features')
expect(entries[0].sections[1].title).toBe('Fixes')
})
})
describe('isVersionNewer', () => {
it('returns true когда seen is null', () => {
expect(isVersionNewer('2.14.0', null)).toBe(true)
})
it('compares major.minor.patch correctly', () => {
expect(isVersionNewer('2.14.0', '2.13.0')).toBe(true)
expect(isVersionNewer('2.14.1', '2.14.0')).toBe(true)
expect(isVersionNewer('3.0.0', '2.99.99')).toBe(true)
expect(isVersionNewer('2.13.0', '2.14.0')).toBe(false)
expect(isVersionNewer('2.14.0', '2.14.0')).toBe(false)
})
it('strips v prefix', () => {
expect(isVersionNewer('v2.14.0', '2.13.0')).toBe(true)
expect(isVersionNewer('2.14.0', 'v2.13.0')).toBe(true)
})
it('returns false для unparseable formats (treat as not newer)', () => {
expect(isVersionNewer('dev', '2.14.0')).toBe(false)
expect(isVersionNewer('2.14.0', 'dev')).toBe(false)
expect(isVersionNewer('local', 'unknown')).toBe(false)
})
})
@@ -0,0 +1,224 @@
import { useMemo } from 'react'
// Public CHANGELOG (human-readable, без commit hashes / scope prefixes,
// curated handler-friendly text). Lives в `docs/changelog.md` чтобы single
// source of truth для public-facing changelog был один (docs build тоже
// его публикует на /docs/changelog.html через Diplodoc).
//
// Internal CHANGELOG (semantic-release auto-generated, с commit hashes для
// engineering team) — `ordinis-admin-ui/CHANGELOG.md`. Не bundled в UI.
// eslint-disable-next-line import/no-unresolved -- Vite ?raw query resolved at build
import changelogRaw from '../../../../docs/changelog.md?raw'
/**
* Parsed CHANGELOG entry — one version block from semantic-release output.
*
* <p>Source format (semantic-release default):
* <pre>
* ## [2.14.0](https://git.nstart.cloud/.../compare/v2.13.0...v2.14.0) (2026-05-12)
*
* ### ✨ Features
*
* * **webhook:** time-series histogram stats endpoint ([5479142](...))
*
* ### 🐛 Fixes
*
* * **ux:** friendly error UI вместо raw AxiosError ([3984d99](...))
* </pre>
*
* <p>Parsed → structured for «What's New» modal rendering.
*/
export type ChangelogEntry = {
/** Version string без 'v' префикса (e.g. "2.14.0"). */
version: string
/** Compare link к git diff между previous и этой версии. */
compareUrl?: string
/** Release date (ISO yyyy-mm-dd) если parsed из header'а. */
date?: string
/** Категория → bullets. Категории: "Features", "Fixes", "Performance", etc. */
sections: ChangelogSection[]
}
export type ChangelogSection = {
/** Display title без emoji (e.g. "Features"). */
title: string
/** Emoji preserved if present (e.g. "✨"). */
emoji?: string
items: ChangelogItem[]
}
export type ChangelogItem = {
/** Scope если был в commit'е (e.g. "webhook", "ui"). */
scope?: string
/** Description без scope + без commit hash link. */
description: string
/** Git commit hash (8 chars). */
commit?: string
/** Full commit URL. */
commitUrl?: string
}
/**
* Parse CHANGELOG.md text → structured ChangelogEntry[].
*
* <p>Format assumptions (semantic-release default):
* <ul>
* <li>Version header: {@code ## [2.14.0](url) (yyyy-mm-dd)}</li>
* <li>Section header: {@code ### ✨ Features} (emoji optional)</li>
* <li>Item: {@code * **scope:** description ([hash](url))}</li>
* </ul>
*
* <p>Robust against missing parts: пустые scope, missing commit URL,
* missing date. Returns entries в порядке появления (newest first для
* semantic-release output).
*/
export function parseChangelog(raw: string): ChangelogEntry[] {
const entries: ChangelogEntry[] = []
// Split на version headers — каждый ## [x.y.z] block.
const versionRegex = /^##\s+\[([^\]]+)\](?:\(([^)]+)\))?\s*(?:\(([\d-]+)\))?/gm
const matches: { start: number; end: number; version: string; url?: string; date?: string }[] = []
let m: RegExpExecArray | null
while ((m = versionRegex.exec(raw)) !== null) {
matches.push({
start: m.index,
end: m.index, // tentative, finalized below
version: m[1],
url: m[2],
date: m[3],
})
}
// Finalize end indices — каждый block заканчивается где начинается следующий
for (let i = 0; i < matches.length; i++) {
matches[i].end = i + 1 < matches.length ? matches[i + 1].start : raw.length
}
for (const block of matches) {
const body = raw.substring(block.start, block.end)
const sections = parseSections(body)
entries.push({
version: block.version,
compareUrl: block.url,
date: block.date,
sections,
})
}
return entries
}
function parseSections(body: string): ChangelogSection[] {
const sectionRegex = /^###\s+(?:(\p{Emoji_Presentation}|\p{Extended_Pictographic})\s+)?(.+)$/gmu
const sections: ChangelogSection[] = []
const matches: { start: number; end: number; emoji?: string; title: string }[] = []
let m: RegExpExecArray | null
while ((m = sectionRegex.exec(body)) !== null) {
matches.push({
start: m.index,
end: m.index,
emoji: m[1],
title: m[2].trim(),
})
}
for (let i = 0; i < matches.length; i++) {
matches[i].end = i + 1 < matches.length ? matches[i + 1].start : body.length
}
for (const s of matches) {
const sectionBody = body.substring(s.start, s.end)
const items = parseItems(sectionBody)
sections.push({ title: s.title, emoji: s.emoji, items })
}
return sections
}
const ITEM_REGEX = /^\*\s+(?:\*\*([^:]+?):\*\*\s+)?(.+?)(?:\s+\(\[([a-f0-9]{7,})\]\(([^)]+)\)\))?\s*$/gm
function parseItems(sectionBody: string): ChangelogItem[] {
const items: ChangelogItem[] = []
let m: RegExpExecArray | null
const regex = new RegExp(ITEM_REGEX.source, ITEM_REGEX.flags)
while ((m = regex.exec(sectionBody)) !== null) {
items.push({
scope: m[1]?.trim(),
description: m[2].trim(),
commit: m[3],
commitUrl: m[4],
})
}
return items
}
/**
* Hook: returns parsed CHANGELOG entries. Memoized — parse runs once per
* page load (CHANGELOG bundled into build, не fetched runtime).
*
* <p>Entries sorted newest-first (semantic-release output order).
*/
export function useChangelog(): ChangelogEntry[] {
return useMemo(() => parseChangelog(changelogRaw), [])
}
/**
* LocalStorage key для tracking last-seen version. Per-user (browser-scoped).
* Если key empty / missing — пользователь никогда не открывал What's New →
* unread = ALL entries (limited к recent N в UI).
*/
const LS_KEY = 'ord-whatsnew-last-seen'
/**
* Returns version string from localStorage, или null если не set.
*/
export function readLastSeenVersion(): string | null {
try {
return localStorage.getItem(LS_KEY)
} catch {
return null
}
}
/**
* Persist last-seen version. Called когда user открывает What's New modal
* AND видит latest entry. Throttle через just writing once per session
* (modal close persists current latest version → next session checks delta).
*/
export function writeLastSeenVersion(version: string): void {
try {
localStorage.setItem(LS_KEY, version)
} catch {
// localStorage может быть disabled (Safari private mode) — fail silently
}
}
/**
* Compare versions для unread detection. Semantic versioning major.minor.patch.
* Returns true если {@code candidate} newer than {@code seen}.
*
* <p>Robust к unknown formats: «dev», «local», missing parts. Returns false
* (treat as not-newer) для unparseable.
*/
export function isVersionNewer(candidate: string, seen: string | null): boolean {
if (!seen) return true // нет baseline → все entries unread
const parse = (v: string): number[] | null => {
const m = v.match(/^v?(\d+)\.(\d+)\.(\d+)/)
if (!m) return null
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)]
}
const a = parse(candidate)
const b = parse(seen)
if (!a || !b) return false
for (let i = 0; i < 3; i++) {
if (a[i] > b[i]) return true
if (a[i] < b[i]) return false
}
return false
}
/**
* Compute count of entries newer than last seen — для notification dot
* на «What's New» button.
*/
export function useUnreadCount(): number {
const entries = useChangelog()
return useMemo(() => {
const seen = readLastSeenVersion()
return entries.filter((e) => isVersionNewer(e.version, seen)).length
// eslint-disable-next-line react-hooks/exhaustive-deps -- seen из localStorage, не trigger'ит re-render автоматически
}, [entries])
}
+22
View File
@@ -24,6 +24,18 @@ i18n
'nav.section.admin': 'Администрирование',
'topbar.search.label': 'Поиск',
'topbar.search.placeholder': 'Поиск по справочникам, ⌘K',
'topbar.notifications': 'Уведомления',
'whatsnew.button': 'Что нового',
'whatsnew.title': 'Что нового в Ordinis',
'whatsnew.description': 'Последние релизы продукта. Обновления автоматически попадают на ваш сервер при следующем deploy.',
'whatsnew.unread_one': '{{count}} новое',
'whatsnew.unread_few': '{{count}} новых',
'whatsnew.unread_many': '{{count}} новых',
'whatsnew.unread_other': '{{count}} новых',
'whatsnew.newBadge': 'новое',
'whatsnew.empty': 'Список релизов пока пуст.',
'whatsnew.entryEmpty': 'Описание не указано.',
'whatsnew.footer': 'Полный список изменений — см. CHANGELOG.md в репозитории.',
'graph.title': 'Граф связей справочников',
'graph.description': 'Сетевая диаграмма всех справочников и их FK-связей. Кликните по узлу — откроется детальный вид.',
'graph.comingSoon.title': 'Скоро',
@@ -734,6 +746,16 @@ i18n
'nav.section.admin': 'Admin',
'topbar.search.label': 'Search',
'topbar.search.placeholder': 'Search dictionaries, ⌘K',
'topbar.notifications': 'Notifications',
'whatsnew.button': "What's new",
'whatsnew.title': "What's new in Ordinis",
'whatsnew.description': 'Latest releases. Updates land on your server with the next deploy.',
'whatsnew.unread_one': '{{count}} new',
'whatsnew.unread_other': '{{count}} new',
'whatsnew.newBadge': 'new',
'whatsnew.empty': 'No releases yet.',
'whatsnew.entryEmpty': 'No description.',
'whatsnew.footer': 'Full changelog — see CHANGELOG.md in repository.',
'graph.title': 'Dictionary relations graph',
'graph.description': 'Network diagram of all dictionaries and their FK links. Click a node for detail.',
'graph.comingSoon.title': 'Coming soon',
+6
View File
@@ -74,6 +74,12 @@ export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
fs: {
// Allow importing files outside admin-ui (e.g. ../docs/changelog.md
// для WhatsNewButton — single source of truth для public changelog
// живёт в docs/ чтобы mkdocs тоже его publish'ил).
allow: ['..'],
},
proxy: {
'/api': {
target: process.env.VITE_API_PROXY ?? 'http://localhost:8080',