From a34c6d4c8248040f90fc24597317d748195468b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=97=D0=B8=D0=BC=D0=B8=D0=BD?= Date: Tue, 12 May 2026 17:05:24 +0000 Subject: [PATCH] =?UTF-8?q?feat(topbar):=20=C2=AB=D0=A7=D1=82=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=C2=BB=20button=20+=202026-?= =?UTF-8?q?05-12=20state.md=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design/ai-schema-assist.md | 0 .../design/dictionary-marketplace.md | 0 .../design/redis-fk-projection.md | 0 docs-internal/status/2026-05-12-state.md | 184 +++++++++++++ docs/changelog.md | 88 +++++++ docs/index.md | 56 +++- docs/operator/audit-log.md | 95 +++++++ docs/operator/close-record.md | 85 ++++++ docs/operator/create-dictionary.md | 118 +++++++++ docs/operator/edit-record.md | 101 ++++++++ docs/operator/errors.md | 144 +++++++++++ docs/operator/index.md | 53 ++++ docs/reviewer/index.md | 70 +++++ docs/reviewer/schema-drafts.md | 243 ++++++++++++++++++ docs/toc.yaml | 27 +- .../src/components/layout/TopBar.tsx | 5 + .../src/components/version/WhatsNewButton.tsx | 207 +++++++++++++++ .../components/version/useChangelog.test.ts | 133 ++++++++++ .../src/components/version/useChangelog.ts | 224 ++++++++++++++++ ordinis-admin-ui/src/i18n.ts | 22 ++ ordinis-admin-ui/vite.config.ts | 6 + 21 files changed, 1853 insertions(+), 8 deletions(-) rename {docs => docs-internal}/design/ai-schema-assist.md (100%) rename {docs => docs-internal}/design/dictionary-marketplace.md (100%) rename {docs => docs-internal}/design/redis-fk-projection.md (100%) create mode 100644 docs-internal/status/2026-05-12-state.md create mode 100644 docs/changelog.md create mode 100644 docs/operator/audit-log.md create mode 100644 docs/operator/close-record.md create mode 100644 docs/operator/create-dictionary.md create mode 100644 docs/operator/edit-record.md create mode 100644 docs/operator/errors.md create mode 100644 docs/operator/index.md create mode 100644 docs/reviewer/index.md create mode 100644 docs/reviewer/schema-drafts.md create mode 100644 ordinis-admin-ui/src/components/version/WhatsNewButton.tsx create mode 100644 ordinis-admin-ui/src/components/version/useChangelog.test.ts create mode 100644 ordinis-admin-ui/src/components/version/useChangelog.ts diff --git a/docs/design/ai-schema-assist.md b/docs-internal/design/ai-schema-assist.md similarity index 100% rename from docs/design/ai-schema-assist.md rename to docs-internal/design/ai-schema-assist.md diff --git a/docs/design/dictionary-marketplace.md b/docs-internal/design/dictionary-marketplace.md similarity index 100% rename from docs/design/dictionary-marketplace.md rename to docs-internal/design/dictionary-marketplace.md diff --git a/docs/design/redis-fk-projection.md b/docs-internal/design/redis-fk-projection.md similarity index 100% rename from docs/design/redis-fk-projection.md rename to docs-internal/design/redis-fk-projection.md diff --git a/docs-internal/status/2026-05-12-state.md b/docs-internal/status/2026-05-12-state.md new file mode 100644 index 0000000..9776e28 --- /dev/null +++ b/docs-internal/status/2026-05-12-state.md @@ -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: `..` (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. diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..412c7ec --- /dev/null +++ b/docs/changelog.md @@ -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). diff --git a/docs/index.md b/docs/index.md index cb14627..670ecc1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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) — часто там сразу ответ ## Версионирование diff --git a/docs/operator/audit-log.md b/docs/operator/audit-log.md new file mode 100644 index 0000000..047a4e3 --- /dev/null +++ b/docs/operator/audit-log.md @@ -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. diff --git a/docs/operator/close-record.md b/docs/operator/close-record.md new file mode 100644 index 0000000..a3f045f --- /dev/null +++ b/docs/operator/close-record.md @@ -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-консьюмеры узнают diff --git a/docs/operator/create-dictionary.md b/docs/operator/create-dictionary.md new file mode 100644 index 0000000..61206b6 --- /dev/null +++ b/docs/operator/create-dictionary.md @@ -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//records` +- Kafka-топик начинает получать события об изменениях записей +- Audit log начинает писать историю операций + +## Чек-лист перед созданием + +- [ ] Машинное имя в snake_case +- [ ] Все обязательные поля помечены в `required` +- [ ] Бизнес-ключ задан (`x-id-source` или поле в `required`) +- [ ] Локализованные поля имеют `x-localized: true` +- [ ] FK-поля указывают на существующие справочники +- [ ] Описания (`description`) даны для каждого поля — это твоя памятка + и помощь следующему оператору + +## Что НЕ стоит делать + +- ❌ Создавать справочник «на всякий случай» без планируемых записей +- ❌ Класть бизнес-логику в схему (валидации вида «если X то Y») — это + приведёт к жёсткой связке, тяжело менять +- ❌ Зашивать константы в `description` — описание читается оператором, + но валидацию по нему не построишь diff --git a/docs/operator/edit-record.md b/docs/operator/edit-record.md new file mode 100644 index 0000000..92d17e0 --- /dev/null +++ b/docs/operator/edit-record.md @@ -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). diff --git a/docs/operator/errors.md b/docs/operator/errors.md new file mode 100644 index 0000000..d7afb72 --- /dev/null +++ b/docs/operator/errors.md @@ -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** — данные не сохранятся всё равно, + но можешь напугать себя +- ❌ **Заводить дубликат записи «чтобы попробовать»** — если первая не + сохранилась, она и не сохранилась, дубликат не появится diff --git a/docs/operator/index.md b/docs/operator/index.md new file mode 100644 index 0000000..08c72fa --- /dev/null +++ b/docs/operator/index.md @@ -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) | diff --git a/docs/reviewer/index.md b/docs/reviewer/index.md new file mode 100644 index 0000000..1487a15 --- /dev/null +++ b/docs/reviewer/index.md @@ -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 влияют на зависимые справочники diff --git a/docs/reviewer/schema-drafts.md b/docs/reviewer/schema-drafts.md new file mode 100644 index 0000000..fb269af --- /dev/null +++ b/docs/reviewer/schema-drafts.md @@ -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). diff --git a/docs/toc.yaml b/docs/toc.yaml index f34e0b6..ee36daf 100644 --- a/docs/toc.yaml +++ b/docs/toc.yaml @@ -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: Обзор diff --git a/ordinis-admin-ui/src/components/layout/TopBar.tsx b/ordinis-admin-ui/src/components/layout/TopBar.tsx index 1b54f80..2f949bc 100644 --- a/ordinis-admin-ui/src/components/layout/TopBar.tsx +++ b/ordinis-admin-ui/src/components/layout/TopBar.tsx @@ -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 }) { + @@ -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 появится. + * + *

«Что нового» (WhatsNewButton) — adjacent button показывает release + * notes, отделён от operational notifications (разные mental models). */ function NotificationsBell() { const { t } = useTranslation() diff --git a/ordinis-admin-ui/src/components/version/WhatsNewButton.tsx b/ordinis-admin-ui/src/components/version/WhatsNewButton.tsx new file mode 100644 index 0000000..2b28233 --- /dev/null +++ b/ordinis-admin-ui/src/components/version/WhatsNewButton.tsx @@ -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. + * + *

Behavior: + *

    + *
  • Button показывает sparkle icon + red dot если есть unread versions + * (entries newer than {@code localStorage.ord-whatsnew-last-seen}).
  • + *
  • Click → modal с list of versions, newest first, max 10 recent.
  • + *
  • Modal open → mark latest version как seen (persisted в localStorage).
  • + *
  • Дальнейшие visits: dot не показывается пока новый release tag не shipped.
  • + *
+ * + *

Source: CHANGELOG.md bundled через Vite {@code ?raw} import → parsed + * клиент-side. No backend changes. + * + *

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(() => 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 ( + <> + + + setOpen(false)} + title={ + + + {t('whatsnew.title', { defaultValue: 'Что нового в Ordinis' })} + + } + description={t('whatsnew.description', { + defaultValue: + 'Последние релизы продукта. Обновления автоматически попадают на ваш сервер при следующем deploy.', + })} + maxWidth="max-w-2xl" + > +

+ {recent.length === 0 ? ( +
+ {t('whatsnew.empty', { + defaultValue: 'Список релизов пока пуст.', + })} +
+ ) : ( +
    + {recent.map((entry) => ( +
  • + +
  • + ))} +
+ )} +
+
+ {t('whatsnew.footer', { + defaultValue: 'Полный список изменений — см. CHANGELOG.md в репозитории.', + })} +
+ + + ) +} + +function EntryCard({ entry, isUnread }: { entry: ChangelogEntry; isUnread: boolean }) { + const { t } = useTranslation() + return ( +
+
+ + v{entry.version} + + {entry.date && ( + {entry.date} + )} + {isUnread && ( + + {t('whatsnew.newBadge', { defaultValue: 'новое' })} + + )} +
+ {entry.sections.length === 0 ? ( +
+ {t('whatsnew.entryEmpty', { + defaultValue: 'Описание не указано.', + })} +
+ ) : ( +
+ {entry.sections.map((section, i) => ( + + ))} +
+ )} +
+ ) +} + +function SectionBlock({ section }: { section: ChangelogSection }) { + return ( +
+

+ {section.emoji && {section.emoji}} + {section.title} +

+
    + {section.items.map((item, i) => ( +
  • + + + {item.scope && ( + {item.scope}: + )} + {item.description} + {item.commit && item.commitUrl && ( + + {item.commit.substring(0, 7)} + + )} + +
  • + ))} +
+
+ ) +} diff --git a/ordinis-admin-ui/src/components/version/useChangelog.test.ts b/ordinis-admin-ui/src/components/version/useChangelog.test.ts new file mode 100644 index 0000000..b568ba2 --- /dev/null +++ b/ordinis-admin-ui/src/components/version/useChangelog.test.ts @@ -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) + }) +}) diff --git a/ordinis-admin-ui/src/components/version/useChangelog.ts b/ordinis-admin-ui/src/components/version/useChangelog.ts new file mode 100644 index 0000000..0a4ce82 --- /dev/null +++ b/ordinis-admin-ui/src/components/version/useChangelog.ts @@ -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. + * + *

Source format (semantic-release default): + *

+ * ## [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](...))
+ * 
+ * + *

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[]. + * + *

Format assumptions (semantic-release default): + *

    + *
  • Version header: {@code ## [2.14.0](url) (yyyy-mm-dd)}
  • + *
  • Section header: {@code ### ✨ Features} (emoji optional)
  • + *
  • Item: {@code * **scope:** description ([hash](url))}
  • + *
+ * + *

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). + * + *

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}. + * + *

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]) +} diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts index f1df44b..9c06035 100644 --- a/ordinis-admin-ui/src/i18n.ts +++ b/ordinis-admin-ui/src/i18n.ts @@ -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', diff --git a/ordinis-admin-ui/vite.config.ts b/ordinis-admin-ui/vite.config.ts index 5b5e084..cb2bbb1 100644 --- a/ordinis-admin-ui/vite.config.ts +++ b/ordinis-admin-ui/vite.config.ts @@ -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',