2fc9b451e4
Альтум-команда обнаружила что /api/v1/dictionaries/{name}/records даёт 500
и spacecraft_codes ссылаются на несуществующий "RESURS-P-3". Оба бага мои:
1. Read-api живёт на /api/v1/{name}/records (БЕЗ /dictionaries/ префикса).
/dictionaries/* — это admin-write контроллер, у него GET-list нет.
Fix: docs/integration/altum-imaging-order.md + docs/tech/api-integration.md
обновлены с правильными URL'ами + примером ответа из live staging.
2. spacecraft seed имел только 4 КА (Terra, Sentinel-1A/2A, Кондор-ФКА),
а mission.spacecraft_codes ссылался на пустые/RESURS-P-3 — каскад в
"Заказ съёмки" не работал бы.
Fix: добавлено 10 российских КА (Ресурс-П 1-4, Канопус-В 1+5, Метеор-М 2+2-2,
Электро-Л 2, Арктика-М 1) с COSPAR designators как businessKey.
3. mission.records.json + instrument.records.json: spacecraft_codes теперь
ссылается на реально существующие COSPAR-ключи. Каскад работает:
instrument MSU-MR → spacecraft [2014-037A, 2019-038A] → Метеор-М № 2 / 2-2.
После redeploy и идемпотентного пересева:
spacecraft 14 (было 4, +10 РФ)
mission 9 (existing 9, references фиксированы)
instrument 11 (было 10, +МСУ-ГСМ Арктики)
264 lines
14 KiB
Markdown
264 lines
14 KiB
Markdown
# Интеграция «Заказ съёмки» (Альтум) ↔ Ordinis НСИ
|
||
|
||
**Status:** схемы согласованы, ожидаем service-account Keycloak и сидинг.
|
||
**Owner со стороны Ordinis:** zimin.an@nstart.space.
|
||
**Owner со стороны Альтума:** altum-backend (FastAPI BFF).
|
||
|
||
## 1. Зачем
|
||
|
||
В форме «Заказ съёмки» (greenfield на стороне Альтума — поля mission /
|
||
instrument / processing_level / data_format пока отсутствуют в БД и UI) поля
|
||
будут читаться из Ordinis НСИ — единый источник истины. Когда оператор НСИ
|
||
заводит новый КА «Ресурс-П 4» — он сразу появляется в выпадашке заказа без
|
||
релиза Альтума.
|
||
|
||
**Польза:** новый спутник / станция / прибор появляется в заказе через 5 минут
|
||
после ввода в админке Ordinis вместо релизного цикла Альтума.
|
||
|
||
## 2. Контракт API (Ordinis → Альтум)
|
||
|
||
**Base URL:**
|
||
|
||
- staging: `https://ordinis.k8s.265.nstart.cloud/api/v1`
|
||
- prod: `https://ordinis.k8s.264.nstart.cloud/api/v1` (после prod stand-up)
|
||
|
||
**Авторизация:**
|
||
|
||
- На staging пока выключена (`ORDINIS_AUTH_REQUIRED=false`), на prod включится.
|
||
- Header `Authorization: Bearer <jwt>` от Keycloak (`auth.nstart.space/realms/nstart`).
|
||
- Альтум использует **service account (m2m client)** в реалме `nstart`, роль
|
||
`ordinis-public`. JWT issued через `client_credentials` flow, рефрешится в
|
||
altum-backend перед TTL.
|
||
- Ordinis маппит роль `ordinis-public` → DataScope.PUBLIC. Маппинг — см.
|
||
`ordinis-auth/.../ScopeContext.java`.
|
||
|
||
**Endpoints (read-only):**
|
||
|
||
> ⚠️ **Read-api живёт без префикса `/dictionaries/`** — это admin-write путь.
|
||
> Для чтения используйте `/api/v1/{name}/records` напрямую.
|
||
|
||
```
|
||
GET /api/v1/dictionaries
|
||
→ список доступных справочников (только метаданные + JSON Schema).
|
||
|
||
GET /api/v1/{name}/records?lang=ru-RU&at=2026-05-04T12:00:00Z
|
||
→ активные записи на момент `at` (default: now).
|
||
?lang=ru-RU / ?lang=en-US — i18n поля сворачиваются в строку на этом языке.
|
||
Без ?lang — i18n остаётся объектом {ru-RU, en-US}, BFF может flatten сам.
|
||
|
||
GET /api/v1/{name}/records/{businessKey}?lang=ru-RU&at=...
|
||
→ одна запись по businessKey.
|
||
|
||
GET /api/v1/{name}/records/{businessKey}/history
|
||
→ все версии записи (закрытые + текущая).
|
||
```
|
||
|
||
**Пример (есть на staging прямо сейчас):**
|
||
|
||
```bash
|
||
curl https://ordinis.k8s.265.nstart.cloud/api/v1/spacecraft/records?lang=ru-RU
|
||
```
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": "72a15bec-56f6-4fb6-8e85-d91a492c707e",
|
||
"businessKey": "1999-068A",
|
||
"data": {
|
||
"name": "Terra (EOS AM-1)",
|
||
"country": "US",
|
||
"operator": "НАСА",
|
||
"norad_id": 25994,
|
||
"designator": "1999-068A",
|
||
"launch_date": "1999-12-18",
|
||
"status": "ACTIVE",
|
||
"satellite_type_code": "OPT_MIDRES_MS",
|
||
"spectrum_bands": ["VIS", "NIR", "SWIR", "TIR"]
|
||
},
|
||
"validFrom": "2026-05-03T20:49:09Z",
|
||
"validTo": "9999-12-31T23:59:59Z"
|
||
}
|
||
]
|
||
```
|
||
|
||
**businessKey формат:** для существующих spacecraft — COSPAR designator
|
||
(например `2016-016A` = Ресурс-П № 3). Для остальных справочников — code-style
|
||
(`RESURS_P`, `MSI-S2`, `GEOTIFF`, `L1A`).
|
||
|
||
## 3. Согласованные схемы 4 справочников
|
||
|
||
Все справочники имеют общую базовую структуру: `code` (string, businessKey),
|
||
`status` (enum: active/archived). Поля name/description — i18n localized.
|
||
Дальше описаны специфические поля.
|
||
|
||
### `mission` — программа / семейство КА
|
||
|
||
| поле | тип | пример | примечание |
|
||
|-----------------------|--------------------|---------------------------------------|---------------------------------------|
|
||
| `code` | string | `RESURS` | businessKey, x-id-source |
|
||
| `name` | i18n | `Ресурс` / `Resurs` | для UI |
|
||
| `description` | i18n | `Российская программа оптического ДЗЗ среднего разрешения` | tooltip |
|
||
| `spacecraft_codes` | array of string | `["RESURS-P-3","RESURS-P-4"]` | businessKeys из spacecraft |
|
||
| `status` | enum | `active` / `archived` | |
|
||
|
||
### `instrument` — конкретный сенсор на борту
|
||
|
||
| поле | тип | пример | примечание |
|
||
|----------------------------|-----------------|-----------------------------------------|----------------------------------------|
|
||
| `code` | string | `GEOTON-L1` | businessKey, x-id-source |
|
||
| `name` | i18n | `Геотон-Л1` | |
|
||
| `spacecraft_codes` | array of string | `["RESURS-P-3","RESURS-P-4"]` | many-to-many |
|
||
| `type` | enum | `optical` / `sar` / `infrared` / `multispectral` / `hyperspectral` | UI-фильтр |
|
||
| `spatial_resolution_m` | number | `1.0` | для сортировки/фильтра |
|
||
| `swath_km` | number | `38` | |
|
||
| `spectral_bands` | array of string | `["PAN","B","G","R","NIR"]` | опц. |
|
||
| `supported_format_codes` | array of string | `["GEOTIFF","NITF"]` | businessKeys из data_format |
|
||
| `supported_level_codes` | array of string | `["L1A","L2"]` | businessKeys из processing_level |
|
||
| `status` | enum | `active` / `archived` | |
|
||
|
||
Связи instrument → format/level в самом instrument'е — даёт фронту cascading
|
||
select без отдельного эндпоинта совместимости.
|
||
|
||
### `processing_level` — уровень обработки
|
||
|
||
| поле | тип | пример |
|
||
|----------------|--------|-------------------------------------------------|
|
||
| `code` | string | `L1A` (businessKey, x-id-source) |
|
||
| `name` | i18n | `Радиометрически скорректированный` |
|
||
| `description` | i18n | (детали обработки) |
|
||
| `sort_order` | integer | `20` (для упорядочивания L0→L1→L2) |
|
||
| `status` | enum | `active` / `archived` |
|
||
|
||
### `data_format` — формат файлов
|
||
|
||
| поле | тип | пример |
|
||
|--------------------|--------|-----------------------------------------|
|
||
| `code` | string | `GEOTIFF` (businessKey, x-id-source) |
|
||
| `name` | i18n | `GeoTIFF` |
|
||
| `mime_type` | string | `image/tiff` |
|
||
| `file_extension` | string | `.tif` |
|
||
| `description` | i18n | опц. |
|
||
| `status` | enum | `active` / `archived` |
|
||
|
||
## 4. Что делает команда Альтум
|
||
|
||
### Backend (altum-backend, FastAPI, единая точка интеграций)
|
||
|
||
- `backend/app/clients/ordinis.py` — httpx-клиент с retry + cache:
|
||
- retry 3× с exponential backoff
|
||
- timeout 5 сек
|
||
- кэш с TTL 5 мин per `(dictionary, lang)` (in-process LRU + ETag когда
|
||
появится; пока без webhook → достаточно TTL)
|
||
- на 401 — refresh service-account JWT через Keycloak
|
||
- на 5xx/таймаут — stale cache + warn в лог. Не падать.
|
||
- `backend/app/routers/reference.py` — единая точка для фронта:
|
||
```
|
||
GET /api/reference/spacecraft
|
||
GET /api/reference/spacecraft/{business_key}
|
||
GET /api/reference/instrument
|
||
GET /api/reference/mission
|
||
GET /api/reference/processing_level
|
||
GET /api/reference/data_format
|
||
```
|
||
Каждый внутри дёргает Ordinis read-api с кэшом. Авторизация JWT-инжекта на
|
||
стороне backend — фронт ничего не знает про Keycloak Ordinis.
|
||
- Таблица `orders` получает новые колонки:
|
||
```
|
||
spacecraft_business_key VARCHAR(256)
|
||
instrument_business_key VARCHAR(256)
|
||
processing_level_code VARCHAR(256)
|
||
data_format_code VARCHAR(256)
|
||
reference_snapshot JSONB
|
||
```
|
||
- Снэпшот на момент создания заказа — копия всех использованных справочных
|
||
записей (для аудита: «через 2 года КА вывели из эксплуатации, но мы должны
|
||
помнить какой был на момент заказа»):
|
||
```json
|
||
{
|
||
"reference_snapshot": {
|
||
"spacecraft": {"RESURS-P-3": {"version": 2, "data": {...}}},
|
||
"instrument": {"GEOTON-L1": {"version": 1, "data": {...}}}
|
||
},
|
||
"captured_at": "2026-05-15T10:30:00Z"
|
||
}
|
||
```
|
||
|
||
### Frontend (TanStack Query)
|
||
|
||
```ts
|
||
const { data: spacecrafts } = useQuery({
|
||
queryKey: ['reference', 'spacecraft', lang],
|
||
queryFn: () => fetch(`/api/reference/spacecraft?lang=${lang}`).then(r => r.json()),
|
||
staleTime: 5 * 60_000,
|
||
})
|
||
|
||
<Select options={spacecrafts.map(s => ({ value: s.businessKey, label: s.data.name }))} />
|
||
```
|
||
|
||
- Skeleton при первой загрузке.
|
||
- Fallback с retry-кнопкой при недоступности backend.
|
||
- `POST /api/orders` — отправляет businessKey'и (не локальные id).
|
||
- Каскадные селекты: instrument → отфильтрованы по выбранному spacecraft через
|
||
`instrument.spacecraft_codes`; processing_level + data_format → по
|
||
`instrument.supported_level_codes` / `supported_format_codes`.
|
||
|
||
### TTL vs webhook
|
||
|
||
5 минут TTL достаточно для v1 — справочники меняются редко (новый КА — раз в
|
||
месяцы). На v2, если потребуется realtime: подписка altum-backend через
|
||
`aiokafka` на топик **`nsi.dictionary.changed`** (Ordinis опубликует) с
|
||
payload `{name, businessKey, action: created|updated|closed}`. Не блокер.
|
||
|
||
### CORS
|
||
|
||
Фронт ходит через altum-backend, прямой CORS не нужен. Для отладки добавим в
|
||
allowlist:
|
||
|
||
- `https://altum.nstart.cloud`
|
||
- `https://altum.k8s.264.nstart.cloud`
|
||
|
||
## 5. Что делает команда Ordinis (todo)
|
||
|
||
- [ ] Сидинг 4 справочников (`mission`, `instrument`, `processing_level`,
|
||
`data_format`) согласно §3 в bundle `cuod`. Отдельный changelog для
|
||
добавления схем + начальные записи.
|
||
- [ ] Service account `altum-backend` в Keycloak realm `nstart` с ролью
|
||
`ordinis-public`. Передать `ORDINIS_CLIENT_ID` / `ORDINIS_CLIENT_SECRET`
|
||
команде Альтума.
|
||
- [ ] CORS allowlist для отладочных доменов Альтума на ingress.
|
||
- [ ] Включить `ORDINIS_AUTH_REQUIRED=true` на staging когда altum-backend
|
||
будет готов тестировать с m2m JWT.
|
||
- [ ] Swagger URL read-api в `docs/tech/api-integration.md` (создать).
|
||
- [ ] (v2) Kafka топик `nsi.dictionary.changed` с change events.
|
||
|
||
## 6. Acceptance criteria
|
||
|
||
- [ ] Оператор НСИ заводит новый КА в Ordinis admin — через ≤5 минут он
|
||
появляется в выпадашке заказа Альтума без редеплоя.
|
||
- [ ] Оператор закрывает КА (`PUT /records/{key}/close`) — через ≤5 минут
|
||
пропадает из выпадашки активных, но в существующих заказах (snapshot)
|
||
всё видно.
|
||
- [ ] Заказ съёмки PDF/выгрузка — берёт name КА из snapshot, не из live read-api.
|
||
- [ ] При недоступности Ordinis Альтум продолжает работать со stale-кэшом,
|
||
выводит баннер «справочники могут быть устаревшими».
|
||
- [ ] Lang switching — оператор переключает на EN, КА называются «Resurs-P № 3».
|
||
- [ ] Cascading select работает: выбираешь инструмент — список форматов и
|
||
уровней обработки сужается.
|
||
|
||
## 7. Сроки
|
||
|
||
- Ordinis: сидинг + Keycloak service account + CORS — **1 день после ответа на §5**.
|
||
- Альтум: BFF + 4 ref-эндпоинта + миграция БД (`reference_snapshot`) + UI с 4
|
||
каскадными селектами + тесты — **2-3 дня** после готовности Ordinis.
|
||
|
||
---
|
||
|
||
**Repo cross-refs:**
|
||
|
||
- Read-api routing: `ordinis-read-api/`
|
||
- Auth/scope: `ordinis-auth/.../{ScopeContext, SecurityConfig}.java`
|
||
- Bitemporal model: `ordinis-domain/.../record/DictionaryRecord.java`
|
||
- Audit log: `ordinis-rest-api/.../audit/AuditLogger.java` (фиксирует все
|
||
изменения справочников — Альтум по trace_id может проследить «когда оператор
|
||
закрыл КА»).
|