docs: Diplodoc setup + полное руководство интеграции по API

Новая документация-as-code на платформе Diplodoc (Yandex open-source) с
YFM (Yandex Flavored Markdown). Build → static HTML, deployable куда угодно.

Что добавлено:
- package.json + .yfm + toc.yaml — Diplodoc setup, навигация для sidebar
- index.md — главная страница с tabs для разных audiences
- README.md — инструкция для contributors

Раздел integration/api/ — 11 страниц про интеграцию consumers:
- index — обзор + глоссарий
- auth — Keycloak service accounts, scope mapping
- read-endpoints — GET /dictionaries, list/by-key, spatial filters
- events — Kafka outbox топики, partition keys, idempotency, DLQ
- webhooks — HMAC, SSRF guard, retry policy, test ping endpoint
- bitemporal — validFrom/validTo + history, future-dated entries, bulk close/export
- x-references — FK syntax, validation errors, scope hiding, orphan scanner, v2 cascade preview
- caching — TTL / push / snapshot strategies, hybrid pattern
- errors — full error code reference (validation/auth/conflict/server), retry strategy
- best-practices — 33 numbered rules для consumers
- bundle-cuod — все 40 справочников с FK графом и сценариями

Обновлены:
- ops/README.md — link на новую integration/api/, убран inline <br>
- ops/slo.md — broken cross-repo links заменены на text references

Build:
  cd docs && pnpm install && pnpm build
  → ./_dist (20 HTML files, 7.1M)
  pnpm serve  # → http://localhost:8088

CI deploy job для GitLab Pages — в roadmap (см. docs/README.md).

Существующий tech/api-integration.md оставлен в toc как legacy
(содержимое мигрировано + расширено в integration/api/).
This commit is contained in:
Zimin A.N.
2026-05-07 23:19:02 +03:00
parent 0c79821f8e
commit d5268b9395
20 changed files with 3450 additions and 5 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
_dist/
.DS_Store
*.log
+12
View File
@@ -0,0 +1,12 @@
langs:
- ru
lang: ru
varsPreset: external
allowHTML: false
needToSanitizeHtml: true
strict: false
ignore:
- node_modules
- _dist
- status/*-state.md
addSystemMeta: true
+109
View File
@@ -0,0 +1,109 @@
# Ordinis docs (Diplodoc / YFM)
Документация Ordinis НСИ ЦУОД ОДХ построена через **Diplodoc** (open-source
documentation platform от Yandex). Markdown source — YFM (Yandex Flavored
Markdown) с расширениями для tabs, notes, includes, mermaid диаграмм.
## Структура
```
docs/
├── package.json # @diplodoc/cli + scripts
├── .yfm # Diplodoc config (langs, plugins)
├── toc.yaml # Содержание / навигация (рендерится в sidebar)
├── index.md # Главная страница
├── integration/ # Интеграция по API
│ ├── api/ # Главы для consumers
│ │ ├── index.md # Обзор
│ │ ├── auth.md # Keycloak / JWT
│ │ ├── read-endpoints.md
│ │ ├── events.md # Kafka outbox
│ │ ├── webhooks.md
│ │ ├── bitemporal.md
│ │ ├── x-references.md
│ │ ├── caching.md
│ │ ├── errors.md
│ │ ├── best-practices.md
│ │ └── bundle-cuod.md
│ └── altum-imaging-order.md # Прикладной сценарий
├── ops/ # Operator runbook
│ ├── README.md
│ ├── slo.md
│ ├── outbox-dlq.md
│ ├── db-migrations.md
│ ├── vault-unseal.md
│ └── kafka-restart.md
├── status/ # State snapshots (исключено из build)
│ └── *-state.md
└── tech/ # Legacy технические страницы
└── api-integration.md
```
## Сборка
Требуется Node 18+.
```bash
cd docs
pnpm install
pnpm build # → ./_dist (HTML)
pnpm serve # → http://localhost:8088
# либо одной командой:
pnpm dev
```
Build output (`_dist/`) — статический HTML, можно деплоить в:
- GitLab Pages
- S3 + CloudFront
- nginx/ingress на cluster.142
## Автор содержимого
| Раздел | Owner |
|---|---|
| `/` (index, integration/api/*) | zimin.an@nstart.space |
| `ops/*` | on-call rotation team |
| `integration/altum-imaging-order.md` | Альтум team contact |
## Что не committable
- `_dist/` — build output (gitignore'нут)
- `node_modules/` — dependencies (gitignore'нут)
## Deployment
В roadmap — GitLab CI job `build-docs`:
```yaml
build-docs:
stage: build
image: node:18
script:
- cd docs
- pnpm install --frozen-lockfile
- pnpm build
artifacts:
paths: [docs/_dist]
expire_in: 1 week
rules:
- changes: ["docs/**/*"]
```
Затем deploy в GitLab Pages либо как отдельный k8s deployment с nginx.
## YFM extensions используются
- `{% list tabs %}` — табы для switching между audiences
- `{% note info|warning|tip %}` — выделенные блоки
- `mermaid` code blocks — диаграммы (graph LR, gantt)
- Cross-links в `toc.yaml` — sidebar navigation
- `varsPreset: external` — для shared includes (TBD when нужно)
## Roadmap
- [ ] CI job `build-docs` в `.gitlab-ci.yml`
- [ ] GitLab Pages deployment
- [ ] Search через Diplodoc search plugin
- [ ] EN translation (i18n via `langs:` в `.yfm`)
- [ ] Includes для shared snippets (auth examples, error tables)
- [ ] Versioning — separate doc trees per Ordinis major version
+62
View File
@@ -0,0 +1,62 @@
# Ordinis НСИ ЦУОД ОДХ
Документация для разработчиков, интеграторов и операторов.
**Anchor v1 — production LIVE на cluster.142** (с 2026-05-07).
Альтум consumers подключены, blue/green cutover завершён.
## Что внутри
{% list tabs %}
- Я интегрирую свой сервис
Начни с [обзора API](integration/api/index.md) и [авторизации](integration/api/auth.md).
Дальше:
- [Read-side endpoints](integration/api/read-endpoints.md) — как читать справочники
- [События (Kafka outbox)](integration/api/events.md) — топики `ordinis.cuod.events.*`
- [Webhooks](integration/api/webhooks.md) — push-уведомления вместо polling
- [Кэширование](integration/api/caching.md) — TTL / push / snapshot стратегии
- Я строю UI / клиент
- [Cross-references](integration/api/x-references.md) — FK между справочниками
- [Bitemporal](integration/api/bitemporal.md) — `validFrom` / `validTo` + история
- [Bundle cuod](integration/api/bundle-cuod.md) — список 40 справочников ЦУОД
- [Best practices](integration/api/best-practices.md) — частые ошибки
- Я — оператор / on-call
- [Runbook indexed by symptom](ops/README.md)
- [SLO + alert thresholds](ops/slo.md)
- [Outbox DLQ](ops/outbox-dlq.md)
- [Database migrations](ops/db-migrations.md)
- [Vault unseal](ops/vault-unseal.md)
- [Kafka restart](ops/kafka-restart.md)
{% endlist %}
## Среды
| Среда | URL | Auth | Назначение |
|---|---|---|---|
| **prod** | `https://ordinis.k8s.264.nstart.cloud/` | JWT обязателен | Альтум real users |
| staging | `https://ordinis.k8s.265.nstart.cloud/` | optional (legacy mode) | Dev environment |
| local | `http://localhost:8080` | off | docker-compose dev |
## Поддержка
- Issues / bug reports: `git.nstart.cloud:2-6/2-6-4/terravault/ordinis`
- Эскалация: `zimin.an@nstart.space`
- Запросы новых ролей / scope в Keycloak: тот же email.
## Версионирование
API стабилизирован под v1 — breaking changes только через major bump (`v2`). См.
[Best practices](integration/api/best-practices.md) для рекомендаций по
обработке появления новых полей в v1 (forward compatibility).
Bundle справочников ЦУОД versioned отдельно (`cuod-bundle:1.2.1`). Изменение
schemas — minor bump, добавление словарей — minor bump, удаление — major bump
(никогда без уведомления consumers через `nsi.dictionary.changed` event).
+112
View File
@@ -0,0 +1,112 @@
# Авторизация
Ordinis принимает JWT от Keycloak realm `nstart`:
- **Issuer:** `https://auth.nstart.space/realms/nstart`
- **Алгоритм:** RS256
- **JWK Set discoverится** через `/.well-known/openid-configuration`
## Service account (m2m)
Для backend-to-backend интеграции (типичный сценарий):
1. Запросить у Ordinis team создание клиента в realm `nstart`.
2. Назначить role: `ordinis-public` / `ordinis-internal` / `ordinis-restricted`
(по нужному scope, см. ниже).
3. Получить `client_id` + `client_secret`. Хранить в Vault либо secret manager,
**никогда** в коде.
## Получение access token
```bash
curl -X POST https://auth.nstart.space/realms/nstart/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=altum-backend" \
-d "client_secret=<SECRET>"
```
Ответ:
```json
{
"access_token": "eyJhbGciOi...",
"expires_in": 300,
"token_type": "Bearer"
}
```
{% note warning %}
Кэшируй токен с TTL = `expires_in - 30` (буфер на retries) в backend
memory. Не делай новый token request на каждый API call — Keycloak rate
limits и латентность.
{% endnote %}
## Использование
Каждый запрос требует header:
```
Authorization: Bearer eyJhbGciOi...
```
Без header → `401 Unauthorized`.
С невалидным/expired токеном → `401`.
Token валиден но scope не позволяет доступ к словарю → `403 Forbidden`.
## Маппинг ролей в DataScope
Ordinis использует трёхуровневый доступ к словарям через `scope` field в
`DictionaryDefinition`:
| Keycloak роль | Доступные scopes |
|---|---|
| `ordinis-public` | `PUBLIC` |
| `ordinis-internal` | `PUBLIC` + `INTERNAL` |
| `ordinis-restricted` | `PUBLIC` + `INTERNAL` + `RESTRICTED` |
Альтернативный синтаксис ролей (для compat с другими сервисами в realm):
суффикс `-PUBLIC|INTERNAL|RESTRICTED` (например `altum-PUBLIC`,
`geoportal-INTERNAL`). Маппинг определяется в `ordinis-auth/.../ScopeContext.java`.
{% note info %}
Альтум consumer на проде использует role `ordinis-internal` — доступ к
PUBLIC + INTERNAL справочникам. RESTRICTED закрытый только для специально
authorized clients.
{% endnote %}
## Refresh tokens
`client_credentials` flow **не поддерживает refresh tokens** — это
m2m flow без user session. Получай новый access token при истечении.
Если используешь user-on-behalf flow (rare для backend) — refresh token
работает, см. Keycloak документацию.
## Multiple environments
| Среда | Issuer | Realm |
|---|---|---|
| prod | `https://auth.nstart.space/realms/nstart` | `nstart` |
| staging | same | same |
| local | same либо local Keycloak (опц) | same |
Один Keycloak realm для всех сред. Service accounts shared (но `client_id`
обычно различается per env: `altum-backend-prod`, `altum-backend-staging`).
## Troubleshooting
| Симптом | Причина | Fix |
|---|---|---|
| 401 на каждый запрос | Token expired | Refresh с TTL buffer |
| 401 на новый token | Invalid client_secret | Re-fetch у Ordinis team |
| 403 на конкретные dictionaries | Scope mismatch | Запросить upgrade role у Ordinis team |
| 500 от Ordinis | Issuer unreachable / JWK fetch fail | Check `auth.nstart.space` health |
## Дальше
→ [Read-side endpoints](read-endpoints.md) — что можно читать с этим токеном.
+156
View File
@@ -0,0 +1,156 @@
# Best practices
Чек-лист для consumer'ов Ordinis API.
## Идентификация записей
1. **Используй `businessKey`, не UUID.** UUID — внутренний идентификатор
записи и **меняется** при создании новой версии (через update).
`businessKey` стабилен через всю жизнь записи.
2. **Не делай FK на копию справочника.** Если копируешь данные к себе для
snapshot — храни `business_key VARCHAR` как key, не `id UUID` либо
`internal_id`. Это устойчиво к updates.
## Авторизация
3. **Кэшируй access token с TTL = `expires_in - 30s`.** Не делай новый
token request на каждый API call — Keycloak rate limits + латентность.
4. **Refresh JWT перед TTL.** Не дожидайся 401 — большинство Keycloak SDK
рефрешат автоматически.
5. **Не храни `client_secret` в коде / git.** Vault либо secret manager.
## Чтение
6. **Кэшируй per `(dictionary, lang)`.** Inline в коде — `staleTime` /
TTL. См. [Caching](caching.md).
7. **Используй `?lang=` параметр** для flatten i18n полей если UI на одном
языке. Без `lang` ответ возвращает `{ru-RU: "...", en-US: "..."}`
объект — лишний размер payload + проще баг.
8. **Не запрашивай history endpoint** на hot path. История нужна только
admin UI / audit. Список + by-key endpoints достаточны для типичного
consumer.
## Запись (admin only)
9. **Не пиши массовых update'ов** через одиночные API calls. Ordinis
оптимизирован под редкие записи и частые чтения. Для bulk import
используй `POST /admin/bundle/import` (через bundle Maven module).
10. **Bulk close через `_bulk-close`** endpoint, не loop одиночных POST.
Per-record transactions, classification (skipped vs error vs closed),
меньше latency.
11. **Bulk export CSV** через `_export?format=csv` для extracting filtered
данные в legacy системы.
## Обработка событий
12. **Idempotency** — события at-least-once, могут прийти дважды. Используй
`(dictionary, businessKey, txTime, version)` либо `deliveryId` (для webhooks)
как dedup key.
13. **Async обработка** — отвечай webhook'у `202 Accepted` сразу, обрабатывай
в background. Жёсткий timeout 10s.
14. **HMAC verify первым** — до парсинга JSON. Защита от spoofed events.
15. **Stale cache на 5xx / timeout** — отдавай старые данные с warn'ом, не
падай каскадно.
## Производительность
16. **Parallel-fetch related dictionaries** при cascading select:
```ts
const [spacecrafts, satTypes, instruments] = await Promise.all([
fetch('/api/v1/spacecraft/records?lang=ru'),
fetch('/api/v1/satellite_type/records?lang=ru'),
fetch('/api/v1/instrument/records?lang=ru'),
])
```
Не последовательные `await`, не N+1 loops.
17. **Cascading filters в memory** — после loading dictionaries фильтруй
клиент-side вместо новых API calls:
```ts
const allowedFormats = formats.filter(f =>
instrument.data.supportedFormatCodes.includes(f.businessKey))
```
18. **Не делай polling в tight loop** — используй webhooks либо Kafka events
для realtime invalidation. Polling каждые 5 мин — OK, каждые 10 сек — нет.
## Error handling
19. **Don't retry 4xx (кроме 401 и 429).** Permanent errors требуют code
fix, не retry.
20. **Exponential backoff на 5xx / timeout.** Base 0.5s, multiplier 2,
jitter 0-0.5s, max 5 attempts. См. [Errors](errors.md).
21. **Сохраняй `traceId`** при эскалации в Ordinis team. Без traceId
investigation в Tempo занимает в 10x дольше.
## Bundle versioning
22. **Forward compatibility** — допускай новые поля в API responses которые
ты не знаешь. Не fail on unknown fields. Schema validation у consumer'а
должна быть `additionalProperties: true` (либо аналог).
23. **Подписывайся на `nsi.dictionary.changed` events** для новых
справочников — они появляются с минор-bump bundle.
24. **Monitor schema versions** в `GET /dictionaries` response — если
`schemaVersion` поменялся, проверь твой mapping не сломался.
## Безопасность
25. **HTTPS обязателен** — webhook URLs тоже только https. SSRF guard
блокирует http:// и private CIDR.
26. **Не log access tokens** в plaintext. Большинство logging frameworks
имеют redaction patterns для `Authorization` header.
27. **Rotate `client_secret`** регулярно. Раз в квартал — minimum, раз
в месяц — recommended для production. Coordinate с Ordinis team
при rotation.
## Коммуникация с командой
28. **Прежде чем bulk import** > 1000 records — talk to Ordinis team. У
нас есть staging environment + bulk-import endpoint без rate limits.
29. **Запрашивай новый scope / role** через `zimin.an@nstart.space`
либо issue с label `keycloak-role-request`.
30. **При проблеме**: воспроизведи на staging, скопируй traceId, открой
issue. Скриншоты UI ошибок помогают.
## Тестирование
31. **Mock Ordinis в unit tests** — не завись от availability staging
cluster. Нашa OpenAPI spec accurate, генерируй mock client.
32. **Integration tests против staging** — не prod. Staging данные — тестовые,
можно create/close без последствий.
33. **Load tests на staging** в low-traffic окно. Координируй с Ordinis
team — мы можем включить дополнительные replicas для вашего теста.
## Reference: bundle cuod
См. [Bundle cuod](bundle-cuod.md) — список 40 справочников, их связи,
типичные operations.
## Дальше
- [События](events.md) — push-обновления
- [Caching](caching.md) — стратегии кэша
- [Errors](errors.md) — обработка ошибок
+185
View File
@@ -0,0 +1,185 @@
# Bitemporal модель
Каждая запись Ordinis имеет двухмерное время:
- **Бизнес-время** (`validFrom` / `validTo`) — когда запись действует в реальности.
- **Транзакционное время** (Hibernate Envers `revinfo.timestamp`) — когда
изменение зафиксировано в БД.
Это две независимые оси: можно ввести запись задним числом
(business → past, txTime → now), либо запланировать на будущее
(business → future, txTime → now), либо retroactively изменить
прошлое (исправить ошибку — txTime → now, но business range уже past).
## Поля
```json
{
"businessKey": "RESURS-P-3",
"validFrom": "2025-01-01T00:00:00+03:00",
"validTo": "9999-12-31T23:59:59+03:00",
"version": 2,
"data": { ... }
}
```
| Поле | Тип | Описание |
|---|---|---|
| `validFrom` | timestamp tz | Когда версия начинает действовать. |
| `validTo` | timestamp tz | До какого момента (или `9999-12-31` если бессрочно). |
| `version` | int | Optimistic lock + порядковый номер версии. Monotonic per businessKey. |
## Update создаёт **новую версию**
Update — это не overwrite. Это:
1. Старая запись: `validTo = now()` (закрыта).
2. Новая запись: `validFrom = now()`, `validTo = 9999...`, `version + 1`.
Обе версии остаются в БД. История доступна через
`GET /records/{key}/history` (см. [Read endpoints](read-endpoints.md)).
```mermaid
gantt
dateFormat YYYY-MM-DD
title Версии записи RESURS-P-3
section v1
v1 (status=in-orbit) :2024-06-01, 2024-12-30
section v2
v2 (status=active) :2024-12-30, 2026-05-07
section v3
v3 (name updated) :2026-05-07, 9999-12-31
```
## Запросы на момент
`?at=2025-06-15` возвращает версию, которая была активна 15 июня 2025:
```bash
curl "https://ordinis.k8s.264.nstart.cloud/api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T00:00:00Z"
```
→ v2 (status=active), потому что `2024-12-30 ≤ 2025-06-15 < 2026-05-07`.
Без `?at` — default = `now()`.
## Future-dated entries
Можно ввести запись в будущее:
```json
PUT /api/v1/dictionaries/spacecraft/records/MISSION-2027
{
"validFrom": "2027-01-01T00:00:00+03:00",
"data": { "name": "Запланированный КА", "status": "planned" }
}
```
Запись существует в БД, но `GET ?at=now` её не вернёт (потому что
`now < validFrom`). Появится автоматически 1 января 2027.
## Закрытие записи (close)
```bash
POST /api/v1/dictionaries/spacecraft/records/RESURS-P-3/close
{
"at": "2026-05-07T12:00:00+03:00",
"reason": "Списан с орбиты"
}
```
Устанавливает `validTo = at`. После этого:
- `GET ?at=2026-05-07T11:00:00Z` → возвращает запись (ещё активна).
- `GET ?at=2026-05-07T13:00:00Z` → 404 (уже закрыта).
- `GET ?at=2026-05-07T11:30:00Z&include_closed=true` → для UI history.
Outbox emit'ит event `action: closed`.
## Bulk close
Закрытие нескольких записей одной транзакцией:
```bash
POST /api/v1/dictionaries/spacecraft/records/_bulk-close
{
"businessKeys": ["RESURS-P-1", "RESURS-P-2", "RESURS-P-3"],
"at": "2026-05-07T00:00:00+03:00",
"reason": "Программа Ресурс-П завершена"
}
```
Response:
```json
{
"closed": 3,
"skipped": [],
"errors": []
}
```
Каждая запись в собственной sub-transaction (Spring AOP per-record).
Если одна fail'нула с invalid_key — она в `errors`, остальные продолжают.
## Bulk export CSV
```bash
GET /api/v1/dictionaries/spacecraft/records/_export?format=csv&lang=ru-RU
```
Stream CSV (RFC 4180) с активными на now записями. Используется UI bulk
operations.
Параметры — те же что list endpoint (`lang`, `at`, фильтры).
## Использование для consumer'ов
### Snapshot at order time
Когда нужен «как было на момент заказа» — два варианта:
**Вариант A: query с `?at=`**
```bash
GET /api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T10:00:00Z
```
Pro: всегда свежие данные если возможна retroactive correction.
Con: depends on Ordinis availability + retention.
**Вариант B: snapshot в свою БД**
```sql
CREATE TABLE imaging_order (
id UUID PRIMARY KEY,
spacecraft_business_key VARCHAR(256),
reference_snapshot JSONB, -- копии записей, не FK
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
Pro: read-only к Ordinis, не зависит от availability/retention.
Con: stale если Ordinis retroactively исправил данные.
Альтум использует **Вариант B** (snapshot) для legal compliance — заказы
attestation требуют не depending on external service.
## Update с конкретным validFrom
Не всегда нужно `validFrom = now()`. Можно указать:
```bash
PUT /api/v1/dictionaries/spacecraft/records/RESURS-P-3
{
"validFrom": "2026-05-15T00:00:00+03:00",
"data": { "name": "Ресурс-П № 3 (новое имя с 15 мая)" }
}
```
Старая версия закрывается с `validTo = 2026-05-15`. Новая `validFrom =
2026-05-15`. До 15 мая `?at=now` возвращает старую, после — новую.
## Дальше
- [Cross-references (FK)](x-references.md) — связи между записями
- [Caching](caching.md) — учитываем bitemporal в инвалидации
+138
View File
@@ -0,0 +1,138 @@
# Bundle справочников cuod (anchor v1)
ЦУОД (Центр обработки данных дистанционного зондирования Земли) — anchor
customer Ordinis v1. Bundle `cuod-bundle:1.2.1` содержит 40 справочников.
## Полный список
| Имя | Scope | Записей (typical) | Назначение |
|---|---|---|---|
| `spacecraft` | PUBLIC | 4-30 | Каталог КА (космические аппараты) |
| `satellite_type` | PUBLIC | 5-15 | Классификация типов КА |
| `spacecraft_status` | PUBLIC | 5-10 | Статусы КА (planned, in-orbit, active, decommissioned) |
| `mission` | PUBLIC | 9-20 | Программы (Ресурс-П, Канопус, Sentinel...) |
| `ground_station` | PUBLIC | 3-10 | Наземные пункты приёма |
| `antenna` | PUBLIC | 5-30 | Антенны на ground stations |
| `instrument` | PUBLIC | 10-50 | Бортовые сенсоры (Геотон, MODIS, C-SAR...) |
| `sensor_type` | PUBLIC | 5-15 | Типы сенсоров (optical, SAR, LIDAR, hyperspectral) |
| `data_format` | PUBLIC | 8-15 | GeoTIFF, NITF, SAFE, COG, JPEG2000, HDF5 |
| `processing_level` | PUBLIC | 8 | L0-L4 уровни обработки |
| `orbit_type` | PUBLIC | 5-10 | LEO, MEO, GEO, SSO, polar |
| `coverage_pattern` | PUBLIC | 5-15 | global, regional, targeted, swath |
| `revisit_period` | PUBLIC | 10-20 | Категории revisit time |
| `band_definition` | PUBLIC | 50-200 | Спектральные band definitions |
| `radiometric_resolution` | PUBLIC | 5-15 | bit depth (8, 12, 16-bit) |
| `swath_width` | PUBLIC | 10-20 | Категории ширины полосы |
| `temporal_resolution` | PUBLIC | 10-20 | Категории временного разрешения |
| `spatial_resolution` | PUBLIC | 15-30 | Категории пространственного разрешения |
| `aoi_type` | PUBLIC | 5-10 | Типы AOI (regions, footprint, custom polygon) |
| `acquisition_mode` | PUBLIC | 5-15 | Modes (push-broom, whisk-broom, frame, SAR strip) |
| `pointing_capability` | PUBLIC | 5-10 | Off-nadir capabilities (nadir-only, ±15°, ±45°) |
| `polarisation_mode` | PUBLIC | 5-10 | SAR polarisation (HH, HV, VH, VV, dual) |
| `antenna_type` | PUBLIC | 10-20 | Антенны: parabolic, phased-array, helical |
| `tracking_mode` | PUBLIC | 5-10 | Tracking algorithms |
| `data_product_type` | PUBLIC | 15-30 | Конечные продукты (orthorectified, mosaic, classified) |
| `coordinate_system` | PUBLIC | 20-50 | Проекции (WGS84, GK-Pulkovo, UTM-zones, MGRS) |
| `agency` | PUBLIC | 10-20 | Космические агентства (Roscosmos, NASA, ESA, JAXA) |
| `orbit_inclination` | PUBLIC | 10 | Категории наклонения |
| `quality_indicator` | INTERNAL | 10-20 | Индикаторы качества (cloud cover, snow cover, off-nadir) |
| `tasking_priority` | INTERNAL | 5-10 | Приоритеты заказов |
| `pricing_tier` | INTERNAL | 5-15 | Тарифы (по resolution / coverage) |
| `customer_segment` | INTERNAL | 10-20 | Сегменты заказчиков |
| `delivery_format` | INTERNAL | 10-20 | Формы поставки (FTP, S3, USB, physical media) |
| `processing_chain_type` | INTERNAL | 10-20 | Цепочки обработки |
| `archive_tier` | INTERNAL | 5 | Tiers хранения (hot/warm/cold) |
| `retention_policy` | RESTRICTED | 5-10 | Сроки хранения по типам данных |
| `access_classification` | RESTRICTED | 5-10 | Уровни секретности |
| `export_control` | RESTRICTED | 5-15 | Экспортные ограничения |
| `licensing_terms` | RESTRICTED | 10-20 | Лицензионные условия |
| `data_sharing_agreement` | RESTRICTED | 5-10 | Соглашения об обмене |
(Точное число записей варьируется — bundle ships seed data, операторы
добавляют свои.)
## Cross-references (FK графа)
```mermaid
graph LR
SC[spacecraft]
ST[satellite_type]
SS[spacecraft_status]
M[mission]
I[instrument]
DF[data_format]
PL[processing_level]
SE[sensor_type]
OT[orbit_type]
AG[agency]
SC --satelliteTypeCode--> ST
SC --statusCode--> SS
SC --orbitTypeCode--> OT
SC --agencyCode--> AG
M --spacecraftCodes--> SC
I --spacecraftCodes--> SC
I --sensorTypeCode--> SE
I --supportedFormatCodes--> DF
I --supportedLevelCodes--> PL
```
(Это subset, для full графа см. отдельные schemas в коде:
`ordinis-cuod-bundle/src/main/resources/cuod/schemas/`.)
## Типичные сценарии
### Сценарий: список всех КА с их типами
1. `GET /api/v1/spacecraft/records?lang=ru` — все КА
2. `GET /api/v1/satellite_type/records?lang=ru` — все типы (parallel)
3. Memory join: `spacecraft.data.satelliteTypeCode === satellite_type.businessKey`
### Сценарий: cascading select для UI
```ts
// User выбрал "instrument: MSI-Sentinel-2"
const instrument = await get('/api/v1/instrument/records/MSI-S2?lang=ru')
// Показать только supported formats
const allFormats = await get('/api/v1/data_format/records?lang=ru')
const allowed = allFormats.filter(f =>
instrument.data.supportedFormatCodes.includes(f.businessKey))
// allowed: SAFE, GEOTIFF, COG, JPEG2000
```
### Сценарий: snapshot для legal compliance
При создании imaging order — snapshot всех использованных справочников
в свою БД. См. [Caching → Snapshot at order time](caching.md#snapshot-at-order-time).
## Performance considerations
- Полный bundle (все 40 справочников × средние 15 записей × ~1KB JSON =
~600KB total). Cold load на startup — допустимо.
- После cold load — кэш на 5 мин TTL, рефреш по событиям через webhook /
Kafka.
- Spatial фильтры (`?bbox=`) — только на словари с geometry полями
(`spacecraft.aoi`, `mission.coverage_area`). Использует PostGIS GiST
index.
## Bundle versioning policy
| Изменение | Bump |
|---|---|
| Add field в schema (optional) | Patch (1.2.1 → 1.2.2) |
| Add справочник | Minor (1.2.x → 1.3.0) |
| Make required field из optional | Minor (consumers compatible если default sane) |
| Remove field / справочник | Major (1.x.x → 2.0.0) |
| Change field type | Major |
| Add x-references | Minor (но requires re-validation existing data) |
Major changes coordinated с consumers через `nsi.dictionary.changed` events
+ admin announcement.
## Дальше
- [Read endpoints](read-endpoints.md) — как читать
- [Cross-references](x-references.md) — детали FK
- [Caching](caching.md) — стратегии кэша для bundle scale
+203
View File
@@ -0,0 +1,203 @@
# Стратегии кэширования
Ordinis оптимизирован под редкие записи и частые чтения. Кэшируй у себя,
не делай round-trip на каждый GET.
Три рекомендуемых стратегии:
## A. Простой TTL (рекомендуется для v1)
Самый простой подход — TTL 5 мин на per-`(dictionary, lang)` ключ.
### TanStack Query (React)
```ts
useQuery({
queryKey: ['ordinis', 'spacecraft', lang],
queryFn: () =>
fetch(`/api/reference/spacecraft?lang=${lang}`, {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json()),
staleTime: 5 * 60_000,
gcTime: 30 * 60_000,
retry: 3,
})
```
### Server-side кэш (Caffeine + Spring)
```java
@Cacheable(value = "ordinis-records", key = "#dictionary + ':' + #lang")
public List<Record> fetchRecords(String dictionary, String lang) {
return ordinisClient.list(dictionary, lang);
}
// application.yml
spring:
cache:
caffeine:
spec: maximumSize=10000,expireAfterWrite=5m
```
**Pro:** просто, работает out of the box.
**Con:** до 5 мин stale данных. При закрытии записи consumer покажет
её ещё 5 мин.
## B. Webhook / Kafka push (рекомендуется для prod)
Realtime invalidation через push-уведомления.
### Kafka
Подпишись на `ordinis.cuod.events.public` (см. [События](events.md)),
для каждого пришедшего event — invalidate соответствующий cache key:
```python
def on_event(event):
dict_name = event['dictionary']
business_key = event['businessKey']
redis.delete(f"ordinis:{dict_name}:list") # invalidate list
redis.delete(f"ordinis:{dict_name}:byKey:{business_key}")
```
### Webhook
Тот же pattern но на HTTP push (см. [Webhooks](webhooks.md)):
```javascript
app.post('/webhooks/ordinis', (req, res) => {
if (!verifyHmac(req)) return res.status(403).send()
for (const event of req.body.events) {
cache.delete(`ordinis:${event.dictionary}:list`)
cache.delete(`ordinis:${event.dictionary}:byKey:${event.businessKey}`)
}
res.status(202).send()
})
```
**Pro:** < 1s invalidation latency.
**Con:** требует Kafka/HTTPS infrastructure + handle duplicates (at-least-once).
## C. Snapshot at order time
Для аудит-кейсов когда нужен «как было на момент заказа» (legal compliance).
### Pattern
```sql
CREATE TABLE imaging_order (
id UUID PRIMARY KEY,
spacecraft_business_key VARCHAR(256),
instrument_business_key VARCHAR(256),
reference_snapshot JSONB, -- копии записей, не FK
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
При создании заказа:
```python
spacecraft_data = ordinis.get(f"/spacecraft/records/{key}")
instrument_data = ordinis.get(f"/instrument/records/{key}")
db.execute(
"INSERT INTO imaging_order(id, spacecraft_business_key, instrument_business_key, reference_snapshot) "
"VALUES (%s, %s, %s, %s)",
(order_id, spacecraft_key, instrument_key, json.dumps({
'spacecraft': spacecraft_data,
'instrument': instrument_data,
}))
)
```
**Pro:** гарантированная история, не зависит от Ordinis availability /
retention. Если Ordinis retroactively изменит запись — твоя копия не
сдвинется.
**Con:** дубликат данных, увеличение твоей БД. Для Альтум typical scale
(~100k orders/year × ~1KB snapshot = 100MB/year) — не проблема.
### Альтернатива: bitemporal query
Можно не делать копию, а позже запрашивать `?at=<order.created_at>`:
```bash
GET /api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T10:00:00Z
```
См. [Bitemporal](bitemporal.md). Pro: меньше storage. Con: depends on
Ordinis availability + retention. Альтум выбрала **snapshot** для legal
compliance.
## Hybrid: TTL + push invalidation
Лучший из двух — TTL 1 час (long enough чтобы сократить request rate) +
event-driven invalidation (для realtime updates):
```python
# Получаем event → invalidate
def on_event(event):
cache.delete(f"ordinis:{event['dictionary']}:list")
# При cache miss — refetch с TTL 1h
def get_records(dictionary, lang):
cached = cache.get(f"ordinis:{dictionary}:list:{lang}")
if cached:
return cached
fresh = ordinis_client.list(dictionary, lang)
cache.setex(f"ordinis:{dictionary}:list:{lang}", 3600, fresh)
return fresh
```
## Cache key patterns
| Что кэшируем | Ключ |
|---|---|
| List of dictionaries | `ordinis:dictionaries:{scope}` |
| List of records per dict | `ordinis:{dict}:list:{lang}` |
| Single record | `ordinis:{dict}:byKey:{businessKey}:{lang}` |
| History of record | `ordinis:{dict}:history:{businessKey}` (rare, TTL=24h) |
Включай `lang` в key — иначе разные locales перетирают друг друга.
## Invalidation на cascade close (v2 roadmap)
В будущем при cascade close источника emit'ятся events для всех
referencing records. Consumer должен слушать events и invalidate их тоже:
```python
def on_event(event):
cache.delete(f"ordinis:{event['dictionary']}:byKey:{event['businessKey']}")
# NEW v2: cascade source — заодно invalidate dependents
if event.get('cascadeSource'):
# Источник тоже нужно invalidate (мы видим только закрытие dependent)
src = event['cascadeSource']
cache.delete(f"ordinis:{src['dictionary']}:byKey:{src['businessKey']}")
```
См. design doc `dict-relationships-v2` (внутренний artifact).
## Stale cache на 5xx / timeout
Если Ordinis недоступен — отдавай старые данные с warn'ом, не падай каскадно:
```python
def get_records_safe(dictionary, lang):
try:
return get_records(dictionary, lang)
except (TimeoutError, HTTPError) as e:
log.warning(f"Ordinis unavailable, using stale cache: {e}")
stale = cache.get(f"ordinis:{dictionary}:list:{lang}", ignore_ttl=True)
if stale:
return stale
raise
```
Метрика `<consumer>_ordinis_stale_serves_total` для мониторинга.
## Дальше
- [Best practices](best-practices.md) — общие рекомендации
- [Bundle cuod](bundle-cuod.md) — что именно нужно кэшировать
+169
View File
@@ -0,0 +1,169 @@
# Ошибки и rate limits
## HTTP status coды
Ordinis следует стандартам REST с расширениями:
| Status | Используется для | Можно retry? |
|---|---|---|
| `200 OK` | GET / PUT успешен | — |
| `201 Created` | POST создан новый ресурс | — |
| `202 Accepted` | Async операция принята | — |
| `204 No Content` | DELETE / close успешен | — |
| `400 Bad Request` | Malformed JSON / missing required fields | Нет — fix payload |
| `401 Unauthorized` | Token expired / invalid / missing | Refresh token и retry |
| `403 Forbidden` | Token валиден, но scope не позволяет | Нет — request higher role |
| `404 Not Found` | Resource не существует / закрыт / out of scope | Зависит от context |
| `409 Conflict` | Optimistic lock fail / cascade block | Refetch + retry |
| `422 Unprocessable Entity` | Validation errors (schema / x-references) | Нет — fix data |
| `429 Too Many Requests` | Rate limit hit | Wait + retry с backoff |
| `500 Internal Server Error` | Server bug | Retry с exponential backoff |
| `502 Bad Gateway` | Upstream (config-server, Vault) недоступен | Retry |
| `503 Service Unavailable` | Postgres недоступен / restart | Retry с long backoff |
| `504 Gateway Timeout` | Запрос > timeout (typically 30s) | Retry |
## Error response format
Все 4xx/5xx с body имеют единый shape:
```json
{
"timestamp": "2026-05-07T15:23:45.123+03:00",
"status": 422,
"error": "Validation Error",
"errors": [
{
"path": "/data/satelliteTypeCode",
"code": "x_references_target_not_found",
"message": "Referenced record not found in 'satellite_type' for code='UNKNOWN'"
},
{
"path": "/data/launchDate",
"code": "schema_format_invalid",
"message": "Expected ISO 8601 date, got 'tomorrow'"
}
],
"traceId": "abc123-def456-789"
}
```
| Поле | Описание |
|---|---|
| `timestamp` | Server time когда ошибка возникла. |
| `status` | HTTP status code. |
| `error` | Категория (`Validation Error`, `Authorization Error` и т.д.). |
| `errors` | Array of detail errors. Может быть один или несколько. |
| `errors[].path` | JSON Pointer к полю с ошибкой (например `/data/launchDate`). |
| `errors[].code` | Machine-readable код для programmatic handling. |
| `errors[].message` | Human-readable описание (русский для admin UI, английский для consumers). |
| `traceId` | Tempo trace ID — приложи при эскалации в Ordinis team. |
## Известные error codes
### Validation (422)
| Code | Описание | Fix |
|---|---|---|
| `schema_required_missing` | Required field пропущен | Добавить поле |
| `schema_format_invalid` | Wrong format (date / email / etc) | Fix format |
| `schema_type_mismatch` | Wrong JSON type (string vs number) | Fix type |
| `x_references_dict_not_found` | Referenced dictionary не существует | Schema bug — escalate |
| `x_references_target_not_found` | Referenced record не активен / out of scope | Use existing key либо upgrade scope |
| `x_references_non_string` | v1 поддерживает только string refs | Fix schema |
| `x_references_malformed` | Bad x-references syntax | Fix schema |
| `x_localized_default_missing` | i18n field без `default_locale` | Add default locale |
| `geometry_invalid_wkt` | Невалидный WKT для geometry | Fix WKT |
| `geometry_invalid_lat_lon` | lat/lon вне диапазона | Use valid coords |
### Authorization (401/403)
| Code | Описание |
|---|---|
| `auth_token_missing` | No `Authorization` header |
| `auth_token_invalid` | Token не verified (signature, issuer) |
| `auth_token_expired` | Token expired |
| `auth_scope_insufficient` | Scope не покрывает запрашиваемый словарь |
### Conflict (409)
| Code | Описание | Fix |
|---|---|---|
| `optimistic_lock_failed` | Запись изменилась между read и write | Refetch + retry |
| `record_already_closed` | close() на уже закрытой записи | No-op либо ignore |
| `bulk_partial_failure` | Bulk операция: часть успешна, часть нет | Check `errors` array в response |
### Server (5xx)
| Code | Описание |
|---|---|
| `internal_unexpected` | Server bug — escalate с traceId |
| `db_unavailable` | Postgres недоступен |
| `vault_unavailable` | Vault sealed либо unreachable |
| `kafka_publish_failed` | Outbox не смог publish (но запись в БД OK — eventually consistent) |
## Rate limits
Сегодня (v1.2.1) Ordinis **не имеет explicit rate limits**. Но есть soft
limits через resource constraints:
| Resource | Limit |
|---|---|
| Postgres connections | 200 (shared между read-api + writer + projection-writer) |
| Read-api app pods | 2× в проде, 1× CPU + 512MB memory |
| Writer pods | 2× в проде |
| Concurrent JWT verifications | bounded heap (~5k/sec практический максимум) |
При превышении (типично DDoS-like patterns):
- HTTP `429 Too Many Requests` после ingress rate limit (ingress nginx default).
- HTTP `503 Service Unavailable` при exhausting connection pool.
- `5xx error rate spike` → alert `OrdinisReadApiErrorBudgetBurnFast`.
### Recommended client-side limits
Чтобы не triggers backend limits:
| Сценарий | Recommended rate |
|---|---|
| Cold load на startup | `≤ 100 req/sec` parallel, then cache |
| Steady state read | `≤ 50 req/sec` per consumer |
| Bulk import / migration | Talk to Ordinis team первым |
Внутренний `RateLimiter` (Caffeine `Bucket4j` — TBD) — в roadmap для v2 если
наблюдаются abuse patterns.
## Retry strategy
```python
import time, random
from typing import Callable
def retry_with_backoff(
fn: Callable, max_attempts: int = 5, base: float = 0.5
) -> any:
for attempt in range(max_attempts):
try:
return fn()
except (HTTPError, TimeoutError) as e:
if attempt == max_attempts - 1:
raise
if e.status_code in (400, 401, 403, 404, 422):
raise # don't retry permanent errors
sleep = base * (2 ** attempt) + random.uniform(0, 0.5)
time.sleep(sleep)
```
## Tracing
Каждый response имеет `X-Trace-Id` header и `traceId` field в error body.
Ordinis эмитит OpenTelemetry traces в Tempo (`tempo.monitoring:4317`).
При эскалации Ordinis team:
1. Скопируй `traceId` из response либо логов.
2. Создай issue с воспроизведением.
3. Ordinis team query'ит Tempo: `traceId="abc123-..."`.
4. Trace покажет full path: ingress → read-api → postgres → response.
## Дальше
- [Best practices](best-practices.md) — как писать robust integration code
- [Авторизация](auth.md) — как избежать 401/403
+189
View File
@@ -0,0 +1,189 @@
# События (Kafka outbox)
Ordinis публикует все изменения справочников в Kafka через
**outbox pattern** (at-least-once гарантия). Consumers подписываются на
топики и инвалидируют свой кэш / обновляют projection.
## Топики
| Топик | Scope | Назначение |
|---|---|---|
| `ordinis.cuod.events.public` | PUBLIC | Изменения публичных справочников. |
| `ordinis.cuod.events.internal` | INTERNAL | INTERNAL словари — нужна role `ordinis-internal`+. |
| `ordinis.cuod.events.restricted` | RESTRICTED | RESTRICTED — role `ordinis-restricted`. |
Topology: 1 broker (single-node на cluster.142), 3+ partitions per topic
(historical setup имел 12 partitions, при cluster recreate 2026-05-07
spec задал 3; existing topics annotated `strimzi.io/managed=false`,
broker partitions сохранены), `cleanup.policy: delete`,
`retention.ms: 604800000` (7 дней).
## Подключение
### Адреса bootstrap
| Среда | Bootstrap servers | Auth |
|---|---|---|
| prod | `ordinis-kafka-kafka-bootstrap.ordinis-prod:9092` (TLS+SASL: `:9093`) | SCRAM-SHA-512 |
| staging | `ordinis-kafka-kafka-bootstrap.ordinis-staging:9092` | SCRAM-SHA-512 |
### Получение SASL креденшелов
Запросить у Ordinis team создание Kafka user (Strimzi `KafkaUser` CR):
```yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaUser
metadata:
name: altum-consumer
namespace: ordinis-prod
labels:
strimzi.io/cluster: ordinis-kafka
spec:
authentication:
type: scram-sha-512
authorization:
type: simple
acls:
- resource:
type: topic
name: ordinis.cuod.events.public
patternType: literal
operations: [Read, Describe]
- resource:
type: group
name: altum-consumer-
patternType: prefix
operations: [Read]
```
Strimzi user-operator создаст Secret `altum-consumer` с полями `username`,
`password`. Используй их в Kafka client config.
## Schema события
### `dictionary.changed`
Все события имеют общий envelope:
```json
{
"schemaVersion": "1.0.0",
"eventType": "dictionary.changed",
"occurredAt": "2026-05-07T15:23:45.123+03:00",
"txTime": "2026-05-07T15:23:45.100+03:00",
"changedBy": "admin@nstart.space",
"changeReason": "Обновление after launch",
"scope": "PUBLIC",
"dictionary": "spacecraft",
"businessKey": "RESURS-P-3",
"action": "updated",
"version": 3,
"validFrom": "2026-05-07T00:00:00+03:00",
"validTo": "9999-12-31T23:59:59+03:00",
"data": {
"name": { "ru-RU": "Ресурс-П № 3 (обновлён)" },
"status": "active",
"satelliteTypeCode": "EARTH-OBSERVATION-OPTICAL"
},
"previousData": {
"name": { "ru-RU": "Ресурс-П № 3" },
"status": "in-orbit"
}
}
```
### Поля envelope
| Поле | Описание |
|---|---|
| `schemaVersion` | Semver схемы события. Bump major = breaking change. |
| `eventType` | `dictionary.changed` для CRUD, `dictionary.bulk_imported` для bundle imports. |
| `occurredAt` | Wall-clock когда событие создано. |
| `txTime` | Transaction time изменения в БД (для ordering). |
| `action` | `created` / `updated` / `closed`. |
| `version` | Optimistic lock version. Monotonic per businessKey. |
| `data` | Payload новой версии. |
| `previousData` | Diff-like — поля, которые поменялись (только для `updated`). Полезен для consumer'ов которые хотят минимальный set re-fetches. |
| `validFrom` / `validTo` | Bitemporal границы. |
### `action` semantics
- **created** — новая запись с уникальным `businessKey`. `previousData = null`.
- **updated** — существующий businessKey, новая версия. `previousData` =
изменённые поля старой версии.
- **closed**`validTo` обновлён в `now()` (раньше `9999-...`). Запись больше
не возвращается на `?at=now`. `previousData` = вся старая версия.
### Cascade events (v2 roadmap)
При cascade close (см. design doc `dict-relationships-v2` — внутренний
artifact в `~/.gstack/projects/claude/`) на каждую закрытую запись
отдельное событие emit'ится в outbox с дополнительным полем
`cascadeSource` = `{dictionary, businessKey}` источника. Consumer может
trace cascade chain.
## Partition key
Сообщения партиционируются по `dictionary + businessKey` (одна запись —
одна partition). Это даёт consumer'ам ordering guarantee per record.
```
key = "spacecraft:RESURS-P-3"
```
{% note info %}
Если consumer обрабатывает события параллельно (multiple consumer threads /
pods), Kafka гарантирует только partition-level ordering. Ordering между
records в разных partitions не определён. Для consumer'ов это норма —
each businessKey изолирован.
{% endnote %}
## At-least-once семантика
Outbox pattern гарантирует **at-least-once** delivery:
1. Writer пишет запись в БД + outbox table в одной транзакции.
2. OutboxPoller (scheduled @1s) читает unpublished rows, отправляет в Kafka.
3. После Kafka ACK — отмечает row как published (`published_at` set).
Что это значит для consumer'а:
- Можешь получить **дубликат** события если poller crashed после ACK но до
`published_at` write. Idempotency у consumer'а — must.
- При network partition между Kafka и poller — сообщения накапливаются в
outbox (alert `OrdinisOutboxLagHigh` срабатывает > 100 events).
## Idempotency
Используй `(dictionary, businessKey, txTime, version)` как dedup key:
```python
seen = redis.get(f"ordinis:event:{dict}:{key}:{tx_time}:{ver}")
if not seen:
process_event(event)
redis.setex(f"ordinis:event:...", 86400, "1")
```
## DLQ (Dead-letter queue)
Если событие не публикуется > 1000 retry попыток (network issues,
Kafka unavailable) — переходит в DLQ table в Ordinis. Не теряется, но
требует ручного разбора.
Симптом: alert `OrdinisOutboxDLQNonEmpty` → check `/api/v1/admin/audit?action=outbox.dlq`.
## Топик для discovery
В roadmap планируется `nsi.dictionary.changed` aggregator topic — single
firehose для consumers которые хотят видеть все scope'ы в одном потоке
(c фильтрацией consumer-side). Сейчас **нет**, подписывайся на 3 scope
топика отдельно.
## Дальше
- [Webhooks](webhooks.md) — alternative для consumer'ов без Kafka access
- [Bitemporal](bitemporal.md) — как интерпретировать `validFrom`/`validTo`
- [Caching](caching.md) — invalidation pattern с Kafka events
+54
View File
@@ -0,0 +1,54 @@
# Интеграция по API — обзор
Этот раздел описывает как backend-сервисы (Альтум, геопортал, BI и т.д.)
читают и пишут справочники Ordinis НСИ ЦУОД.
**Owner:** Ordinis team — `zimin.an@nstart.space`.
## С чего начать
1. [Получи Keycloak service account](auth.md) — нужен для всех API вызовов.
2. [Прочитай read-side endpoints](read-endpoints.md) — как получить данные.
3. [Выбери стратегию обновления](caching.md) — TTL, webhooks, либо Kafka events.
## Что Ordinis даёт
- **Bitemporal CRUD** для справочников — `validFrom` / `validTo` + revision
history через Hibernate Envers.
- **Multi-locale** — каждое поле может иметь `{ru-RU, en-US, ...}` варианты.
Flatten через `?lang=` параметр.
- **Cross-dictionary FK** через `x-references` schema extension. Server-side
валидация на CRUD + orphan scanner periodic.
- **Geometry support** — PostGIS для bbox/polygon фильтров (`spacecraft.aoi`,
`mission.coverage_area`).
- **Audit log** — все изменения через `/api/v1/admin/audit` (UI: `/audit`).
- **Outbox + Kafka** — at-least-once delivery всех изменений в 3 топика.
- **Webhooks** — push-уведомления HTTP к зарегистрированным URLs (для
consumers без Kafka access).
- **Bulk operations** — multi-select close + CSV export.
## Что Ordinis НЕ делает
- Не хранит "снимок на момент заказа" — для этого consumer должен сделать
snapshot rough copy в свою БД (см. [Caching → Snapshot strategy](caching.md#snapshot-at-order-time)).
- Не enforce'ит FK на уровне DB — это NoSQL-style references поверх JSONB.
Cascade rules в roadmap (см. design `dict-relationships-v2`).
- Не обеспечивает realtime sync — TTL = 5 мин минимум либо event-driven invalidation.
## Версионирование
- **API**`/api/v1/...` стабилен. Breaking changes только через major (`v2`).
- **Bundle** (cuod-bundle) — semver. Minor bump при schema изменении, major
при удалении справочника. См. [Bundle cuod](bundle-cuod.md).
## Глоссарий
| Термин | Значение |
|---|---|
| Dictionary | Справочник (например `spacecraft`, `satellite_type`). |
| Record | Запись в справочнике с уникальным `businessKey`. |
| businessKey | Стабильный идентификатор (string). Не меняется при update. |
| Bundle | Maven module с schemas + seed data. ЦУОД bundle = 40 справочников. |
| Scope | Уровень доступа: `PUBLIC` / `INTERNAL` / `RESTRICTED`. |
| Bitemporal | Запись имеет `validFrom`/`validTo` + revision history (transaction time). |
| x-references | JSON Schema extension для cross-dictionary FK. |
+191
View File
@@ -0,0 +1,191 @@
# Read-side endpoints
Read-API в Ordinis — отдельный deployable (`ordinis-read-api`, scaled 2× в проде)
работающий на Postgres replica. Запросы из read-api **не блокируют** writer'а.
Base URL:
| Среда | URL |
|---|---|
| prod | `https://ordinis.k8s.264.nstart.cloud/api/v1` |
| staging | `https://ordinis.k8s.265.nstart.cloud/api/v1` |
Все endpoints требуют JWT (см. [Авторизация](auth.md)).
## OpenAPI / Swagger UI
- **Swagger UI:** `https://ordinis.k8s.264.nstart.cloud/swagger-ui.html`
- **Spec JSON:** `https://ordinis.k8s.264.nstart.cloud/v3/api-docs`
- **Spec YAML:** `https://ordinis.k8s.264.nstart.cloud/v3/api-docs.yaml`
Из spec'а можно generate'ить клиент:
```bash
openapi-generator-cli generate \
-i https://ordinis.k8s.264.nstart.cloud/v3/api-docs \
-g python \
-o ./altum-backend/clients/ordinis
```
Поддерживаются: Python, TypeScript, Go, Java, Kotlin, Ruby и др.
## `GET /api/v1/dictionaries`
Список доступных справочников (фильтрован по scope JWT'а).
```bash
curl -H "Authorization: Bearer $TOKEN" \
https://ordinis.k8s.264.nstart.cloud/api/v1/dictionaries
```
Response:
```json
[
{
"name": "spacecraft",
"displayName": "Космические аппараты",
"scope": "PUBLIC",
"schemaVersion": "1.0.0",
"recordsCount": 4
},
{
"name": "mission",
"displayName": "Миссии / программы",
"scope": "PUBLIC",
"schemaVersion": "1.0.0",
"recordsCount": 9
}
]
```
## `GET /api/v1/{name}/records`
{% note warning %}
**БЕЗ префикса `/dictionaries/`!** Read-api сидит на `/api/v1/{name}/records`.
Префикс `/api/v1/dictionaries/...` — это admin write controller (CRUD), у него
нет bulk-list эндпоинта.
{% endnote %}
Активные записи на момент `at` (default: сейчас).
### Параметры
| Параметр | Тип | Пример | Назначение |
|---|---|---|---|
| `lang` | string | `ru-RU`, `en-US` | Flatten i18n полей в строку. Короткие `ru`/`en` тоже. |
| `at` | ISO 8601 | `2026-05-04T12:00:00+03:00` | Bitemporal query — активные на момент. |
| `as_scope` | string | `PUBLIC,INTERNAL` | Legacy для anonymous + `allowQueryScope=true`. |
### Пример
```bash
curl -H "Authorization: Bearer $TOKEN" \
"https://ordinis.k8s.264.nstart.cloud/api/v1/spacecraft/records?lang=ru-RU"
```
Response:
```json
[
{
"businessKey": "RESURS-P-3",
"displayKey": "Ресурс-П № 3",
"scope": "PUBLIC",
"validFrom": "2025-01-01T00:00:00+03:00",
"validTo": "9999-12-31T23:59:59+03:00",
"data": {
"name": "Ресурс-П № 3",
"satelliteTypeCode": "EARTH-OBSERVATION-OPTICAL",
"launchDate": "2024-12-25",
"status": "active"
},
"version": 2
}
]
```
### Без `?lang` (raw i18n)
```json
"name": { "ru-RU": "Ресурс-П № 3", "en-US": "Resurs-P No.3" }
```
С `?lang=ru-RU`:
```json
"name": "Ресурс-П № 3"
```
Если запрошенного языка нет — fallback к `default_locale` (обычно `ru-RU`).
## `GET /api/v1/{name}/records/{businessKey}`
Одна запись. Параметры те же что у list.
```bash
curl -H "Authorization: Bearer $TOKEN" \
"https://ordinis.k8s.264.nstart.cloud/api/v1/spacecraft/records/RESURS-P-3?lang=ru"
```
`404 Not Found` — запись либо не существует, либо закрыта на момент `at`,
либо вне scope JWT'а (Ordinis скрывает существование).
## `GET /api/v1/{name}/records/{businessKey}/history`
Все версии записи (включая закрытые) для построения истории изменений в UI.
```json
[
{
"version": 1,
"validFrom": "2024-06-01T00:00:00+03:00",
"validTo": "2025-01-01T00:00:00+03:00",
"data": {...},
"txTime": "2024-06-01T10:23:45+03:00",
"changedBy": "admin@nstart.space",
"changeReason": "initial"
},
{
"version": 2,
"validFrom": "2025-01-01T00:00:00+03:00",
"validTo": "9999-12-31T23:59:59+03:00",
"data": {...},
"txTime": "2024-12-30T14:11:22+03:00",
"changedBy": "admin@nstart.space",
"changeReason": "Обновление статуса после успешного запуска"
}
]
```
Для consumers history endpoint обычно не нужен — это UI-feature.
## Spatial filters (geometry)
Если справочник имеет geometry поля (`spacecraft.aoi`, `mission.coverage_area`),
поддерживаются bbox + polygon фильтры:
### `?bbox=` (bounding box)
```
GET /api/v1/spacecraft/records?bbox=37.0,55.0,38.0,56.0
```
Format: `min_lon,min_lat,max_lon,max_lat` (WGS84). Возвращает записи где
`geometry` пересекает bbox через PostGIS `ST_Intersects` + GiST index.
### `?polygon=` (GeoJSON)
```
GET /api/v1/spacecraft/records?polygon={"type":"Polygon","coordinates":[[[37,55],[38,55],[38,56],[37,56],[37,55]]]}
```
Polygon — GeoJSON object (URL-encoded). Сложнее bbox, точнее.
## Дальше
- [События (Kafka outbox)](events.md) — push-обновления вместо polling
- [Bitemporal](bitemporal.md) — детали `validFrom`/`validTo` модели
- [Caching](caching.md) — стратегии актуализации
+216
View File
@@ -0,0 +1,216 @@
# Webhooks
Альтернатива Kafka events для consumer'ов которые не хотят / не могут
поддерживать Kafka client. Ordinis отправляет HTTP POST на зарегистрированный
URL при изменениях справочников.
{% note info %}
Если у тебя есть Kafka access — используй [события](events.md). Webhooks
проще в развёртывании, но менее надёжны (HTTP retry vs Kafka durable log)
и медленнее (HTTP overhead vs Kafka batch).
{% endnote %}
## Регистрация подписки
```bash
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/subscriptions \
-d '{
"name": "altum-cache-invalidation",
"url": "https://altum-backend.altum.local/webhooks/ordinis",
"secret": "<HMAC_SECRET_GENERATED_BY_YOU>",
"scopes": ["PUBLIC", "INTERNAL"],
"dictionaries": ["spacecraft", "satellite_type", "instrument"],
"actions": ["created", "updated", "closed"],
"active": true
}'
```
### Поля
| Поле | Тип | Назначение |
|---|---|---|
| `name` | string | Человекочитаемое имя для admin UI. |
| `url` | string | HTTPS endpoint. HTTP блокируется SSRF guard'ом. |
| `secret` | string | HMAC-SHA256 секрет для подписи payload (см. ниже). |
| `scopes` | array | Фильтр по scope (subset из доступных consumer'у). |
| `dictionaries` | array | Фильтр по dictionary names. `null`/`[]` = все доступные. |
| `actions` | array | `created` / `updated` / `closed`. `null` = все. |
| `active` | bool | Можно временно отключить без удаления. |
## SSRF guard
Webhook URLs валидируются перед отправкой:
- **Запрещено:** private CIDR (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16),
localhost, link-local (169.254/16), metadata IPs.
- **Запрещено:** non-HTTPS scheme. Только `https://`.
- **Запрещено:** URL > 2048 chars.
Метрика `nsi_webhook_ssrf_rejected_total` increments при попытках. См.
prometheus rule `OrdinisWebhookSsrfRejected`.
## Payload
POST на зарегистрированный URL с body:
```json
{
"deliveryId": "wd-abc123-xyz789",
"subscriptionName": "altum-cache-invalidation",
"occurredAt": "2026-05-07T15:23:45.123+03:00",
"events": [
{
"schemaVersion": "1.0.0",
"eventType": "dictionary.changed",
"dictionary": "spacecraft",
"businessKey": "RESURS-P-3",
"action": "updated",
"version": 3,
"validFrom": "2026-05-07T00:00:00+03:00",
"data": { ... }
}
]
}
```
{% note tip %}
Payload может содержать **несколько events** (`events` array) если они
произошли в одной transaction (bulk close, bundle import). Не предполагай
single-event payload.
{% endnote %}
## HMAC подпись
Каждый POST имеет header:
```
X-Ordinis-Signature: sha256=<HEX>
X-Ordinis-Delivery: wd-abc123-xyz789
X-Ordinis-Subscription: altum-cache-invalidation
```
Где `<HEX>` = HMAC-SHA256 от raw body bytes с `secret` который ты задал
при регистрации.
### Verify (Python)
```python
import hmac, hashlib
def verify(body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
```
### Verify (Node.js)
```javascript
import crypto from 'crypto'
function verify(body, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
)
}
```
{% note warning %}
Verify подпись **до** парсинга JSON. Если invalid — `403 Forbidden`,
не трогай payload. Без verify ты уязвим к подделке events от любого, кто
знает твой URL.
{% endnote %}
## Response contract
Consumer должен ответить `2xx` в течение 10 секунд:
| Status | Семантика |
|---|---|
| `200` / `201` / `202` / `204` | Принято. Ordinis помечает delivered. |
| `4xx` | Permanent failure (твой bug). Ordinis НЕ retry. |
| `5xx` / timeout | Transient. Ordinis retry с exponential backoff. |
## Retry policy
| Попытка | Интервал |
|---|---|
| 1 | сразу |
| 2 | через 30s |
| 3 | через 2 мин |
| 4 | через 10 мин |
| 5 | через 1ч |
| 6-10 | каждые 6ч |
| 1000+ | DLQ → ручной разбор |
После 1000 неудач delivery попадает в DLQ (`/api/v1/admin/webhooks/deliveries/dlq`).
Alert: `OrdinisWebhookDLQNonEmpty`.
## Test ping
Endpoint для проверки доступности URL **до** регистрации:
```bash
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/test-ping \
-d '{
"url": "https://altum-backend.altum.local/webhooks/ordinis",
"secret": "<TEST_SECRET>"
}'
```
Response:
```json
{
"success": true,
"statusCode": 200,
"responseBody": "OK",
"elapsedMs": 142
}
```
Либо details про failure (DNS issue, timeout, SSRF reject и т.д.).
## Best practices
1. **Idempotency** — webhook может прийти **дважды** (network retry, partial
failure). Используй `deliveryId` как dedup key.
2. **Async обработка** — отвечай `202 Accepted` сразу, обрабатывай в
background. 10s timeout жёсткий — если БД медленная, лучше принять и
сложить в очередь.
3. **HMAC verify первым** — до парсинга JSON.
4. **Metrics** — track received / processed / errors для своего side.
Сравнивай с Ordinis metrics при расследовании.
5. **Whitelist Ordinis IP** на firewall если строгие правила. IP cluster.142
= `192.168.100.142`, но в production будет ingress IP. Спросить у Ordinis
team при registration.
## Alerting (Ordinis side)
| Alert | Trigger |
|---|---|
| `OrdinisWebhookHighFailureRate` | failure rate > 50% за 5 мин |
| `OrdinisWebhookDLQNonEmpty` | events в DLQ > 0 |
| `OrdinisWebhookDispatcherIdle` | 0 deliveries при > 0 active subscriptions |
| `OrdinisWebhookSsrfRejected` | SSRF guard сработал > 0 раз за 15 мин |
## Дальше
- [События (Kafka)](events.md) — primary delivery channel
- [Caching](caching.md) — webhook invalidation pattern
+142
View File
@@ -0,0 +1,142 @@
# Cross-references (FK между справочниками)
JSON Schema extension `x-references` объявляет, что значение поля должно
ссылаться на active record в указанном словаре.
## Синтаксис
```json
{
"type": "object",
"properties": {
"satelliteTypeCode": {
"type": "string",
"x-references": "satellite_type.code"
}
}
}
```
Format: `"<dictionary_name>.<field_name>"`. Когда POST/PUT приходит, Ordinis
делает lookup в `satellite_type` где `data->>code == satelliteTypeCode`.
Если не найден / не активен / вне scope — `422 Validation Error`.
## Текущие FK в bundle cuod
| Source | Target | Поле |
|---|---|---|
| `spacecraft.satelliteTypeCode` | `satellite_type` | `code` |
| `spacecraft.statusCode` | `spacecraft_status` | `code` |
| `mission.spacecraftCodes` (array) | `spacecraft` | `businessKey` |
| `instrument.spacecraftCodes` (array) | `spacecraft` | `businessKey` |
| `instrument.supportedFormatCodes` (array) | `data_format` | `businessKey` |
| `instrument.supportedLevelCodes` (array) | `processing_level` | `businessKey` |
(Точное число и список могут меняться с bundle versions — см.
[Bundle cuod](bundle-cuod.md).)
## Server-side validation
При POST/PUT ReferenceValidator проверяет каждое x-references поле:
| Симптом | Error code | Описание |
|---|---|---|
| Referenced dictionary не существует | `x_references_dict_not_found` | Опечатка в schema. |
| Referenced record не найден / inactive | `x_references_target_not_found` | Bad value либо record закрыт. |
| Value не string (для v1) | `x_references_non_string` | v1 поддерживает только string refs. |
| Schema spec malformed | `x_references_malformed` | Bad schema. |
Response:
```json
{
"errors": [
{
"path": "/satelliteTypeCode",
"code": "x_references_target_not_found",
"message": "Referenced record not found in 'satellite_type' for code='UNKNOWN-TYPE'"
}
]
}
```
## Scope hiding
Если referenced dictionary — INTERNAL/RESTRICTED, а consumer имеет только
PUBLIC scope, ReferenceValidator возвращает тот же error
(`x_references_target_not_found`) что и на real not-found. Ordinis **не leak'ает**
existence of out-of-scope dictionary через differential errors.
## Orphan reference scanner
Возможна ситуация когда referenced record **закрывается**, а referencing
record остаётся:
```
1. spacecraft.RESURS-P-3 references satellite_type.EARTH-OPTICAL
2. Admin closes satellite_type.EARTH-OPTICAL (record.closed event emitted)
3. spacecraft.RESURS-P-3 теперь имеет dangling FK
```
Periodic scanner (`OrphanReferenceScanner`, runs каждые 5 мин) находит
такие orphan'ы и emit'ит metric:
```
nsi_orphan_references_total{source_dict="spacecraft",target_dict="satellite_type"} 1
```
Alert `OrdinisOrphanReferences` срабатывает при > 0 за 10 мин.
## Cascade rules — roadmap (v2)
Сегодня закрытие источника НЕ closes referencing automatically. Это
дolжно явно делаться через bulk close либо individual updates.
В v2 планируется schema extension `x-references-on-close`:
```json
{
"satelliteTypeCode": {
"type": "string",
"x-references": "satellite_type.code",
"x-references-on-close": "block"
}
}
```
Опции:
- **block** — закрытие источника невозможно пока есть active referencing (default).
- **warn** — закрытие проходит, в response warning + metric `nsi_cascade_warnings_total`.
- **cascade** — все referencing закрываются в той же транзакции.
См. design doc `dict-relationships-v2` (внутренний artifact в
`~/.gstack/projects/claude/`) для полного scope.
## Lineage discovery — roadmap (v2)
Сегодня нет endpoint для "что depends on X". Admin должен вручную проходить
schemas чтобы найти ссылки. В v2 планируется:
- `GET /api/v1/dictionaries/{name}/dependents` — словари которые ссылаются.
- `GET /api/v1/records/{dict}/{key}/dependents` — конкретные записи.
См. design выше для подробностей.
## Best practices
1. **Используй businessKey как target field** для большинства FK. Это
стабильный идентификатор. Альтернатива (`code`, `name`) работает но
требует поддержки `x-unique` в target (иначе ambiguous matches).
2. **Не делай deeply nested refs** — v1 поддерживает только top-level
fields. Nested objects с x-references — пропустят validation.
3. **Array refs работают** — если поле `type: array, items: {type: string,
x-references: ...}`, каждый элемент валидируется. (Но в v1.2.1 это
делается на каждый элемент отдельно — for large arrays медленно. Будет
batch'нуто в v2.)
4. **Document FK в displayName** — `"description": "FK на satellite_type
через code"` помогает при ревью schemas.
## Дальше
- [Bundle cuod](bundle-cuod.md) — список текущих FK в проде
- [События](events.md) — учитывай cascade closure при инвалидации
+3 -3
View File
@@ -71,7 +71,7 @@ kubectl get pods -n ordinis-staging | grep admin-ui
| Status | Причина | | Status | Причина |
|-----------------------|---------| |-----------------------|---------|
| `ImagePullBackOff` | Тег image не существует в registry. Проверь `kubectl describe`. Восстановить:<br>`kubectl set image deployment/ordinis-admin-ui -n ordinis-staging ui=repo.nstart.cloud/terravault/ordinis-admin-ui:main` | | `ImagePullBackOff` | Тег image не существует в registry. Проверь `kubectl describe`. Восстановить: `kubectl set image deployment/ordinis-admin-ui ui=<registry>/ordinis-admin-ui:main -n ordinis-staging` |
| `CrashLoopBackOff` | nginx config invalid либо resolver недоступен. См. logs. | | `CrashLoopBackOff` | nginx config invalid либо resolver недоступен. См. logs. |
| `Running 1/1`, но 502 | Upstream (writer/read-api) сам в дауне. Проверь те pod'ы. | | `Running 1/1`, но 502 | Upstream (writer/read-api) сам в дауне. Проверь те pod'ы. |
@@ -91,8 +91,8 @@ kubectl get pods -n ordinis-staging | grep admin-ui
## 6. Связанные ресурсы ## 6. Связанные ресурсы
- API integration guide: [../tech/api-integration.md](../tech/api-integration.md) - API integration guide: [../integration/api/index.md](../integration/api/index.md) (Diplodoc, latest)
- Альтум integration: [../integration/altum-imaging-order.md](../integration/altum-imaging-order.md) - Альтум integration: [../integration/altum-imaging-order.md](../integration/altum-imaging-order.md)
- Project state: [../status/2026-05-05-state.md](../status/2026-05-05-state.md) - Project state snapshots: см. `docs/status/*-state.md` в repo (исключены из Diplodoc build)
- Helm chart: `ordinis-infra/charts/ordinis/` - Helm chart: `ordinis-infra/charts/ordinis/`
- Vault setup: `ordinis-infra/k8s/vault/setup.sh` - Vault setup: `ordinis-infra/k8s/vault/setup.sh`
+2 -2
View File
@@ -5,7 +5,7 @@
после 2-3 месяцев наблюдения. после 2-3 месяцев наблюдения.
**Source of truth для алертов:** **Source of truth для алертов:**
[ordinis-infra/charts/ordinis/templates/prometheus-rules.yaml](../../code/ordinis-infra/charts/ordinis/templates/prometheus-rules.yaml). `ordinis-infra/charts/ordinis/templates/prometheus-rules.yaml` (отдельный repo).
## 1. SLI (Service Level Indicators) ## 1. SLI (Service Level Indicators)
@@ -63,7 +63,7 @@ Routing в Slack/Telegram/Email — отдельная задача:
- **warning** → digest channel (раз в час summary) - **warning** → digest channel (раз в час summary)
- [ ] Эскалация: critical → engineer → tech lead через 15 мин если не acked. - [ ] Эскалация: critical → engineer → tech lead через 15 мин если не acked.
См. v1 closure backlog в [docs/status/2026-05-05-state.md](../status/2026-05-05-state.md). См. `docs/status/2026-05-05-state.md` в repo (status snapshots исключены из Diplodoc build).
## 3. Error budget (v1 target) ## 3. Error budget (v1 target)
+18
View File
@@ -0,0 +1,18 @@
{
"name": "ordinis-docs",
"version": "1.0.0",
"private": true,
"description": "Ordinis НСИ ЦУОД ОДХ — техническая документация (Diplodoc / YFM)",
"scripts": {
"prebuild": "rm -rf ./_dist",
"build": "yfm build -i . -o ./_dist --output-format html",
"build:md": "yfm build -i . -o ./_dist --output-format md",
"serve": "npx serve ./_dist -p 8088",
"dev": "pnpm build && pnpm serve",
"clean": "rm -rf ./_dist"
},
"devDependencies": {
"@diplodoc/cli": "^4.50.0"
},
"packageManager": "pnpm@9.0.0"
}
+1429
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
title: Ordinis НСИ ЦУОД ОДХ
href: index.md
items:
- name: Введение
href: index.md
- name: Интеграция по API
items:
- name: Обзор
href: integration/api/index.md
- name: Авторизация
href: integration/api/auth.md
- name: Read-side endpoints
href: integration/api/read-endpoints.md
- name: События (Kafka outbox)
href: integration/api/events.md
- name: Webhooks
href: integration/api/webhooks.md
- name: Bitemporal модель
href: integration/api/bitemporal.md
- name: Cross-references (FK)
href: integration/api/x-references.md
- name: Кэширование
href: integration/api/caching.md
- name: Ошибки и rate limits
href: integration/api/errors.md
- name: Best practices
href: integration/api/best-practices.md
- name: Bundle справочников cuod
href: integration/api/bundle-cuod.md
- name: Прикладные сценарии
items:
- name: Альтум — заказ съёмки
href: integration/altum-imaging-order.md
- name: Operations
items:
- name: Runbook (индекс)
href: ops/README.md
- name: SLO
href: ops/slo.md
- name: Outbox DLQ
href: ops/outbox-dlq.md
- name: Database migrations
href: ops/db-migrations.md
- name: Vault unseal
href: ops/vault-unseal.md
- name: Kafka restart
href: ops/kafka-restart.md
- name: Архитектура (legacy)
items:
- name: API integration (старая страница, см. /integration/api/)
href: tech/api-integration.md