Зеркало правки в instrument.spacecraft_codes (MR !257). Список КА программы
теперь рендерится в admin-UI как MultiSelect с поиском по designator
(2014-037A — Ресурс-П №2) вместо comma-separated «businessKey'и из spacecraft».
mission schemaVersion 1.0.0 → 1.1.0, bundle 1.3.3 → 1.3.4.
bundleUpdateExistingSchema=true (MR !90) докатит schema_json на рестарте writer'а.
Три массива в instrument были comma-separated текстом с подсказкой
«businessKey'и из X». Пользователь не знал что туда вписать — справочника
доступных значений рядом нет.
UI: добавлен ReferenceMultiSelectField — array-аналог ReferenceSelectField.
Триггер — `items["x-references"]` на string-массиве. Под капотом — готовый
MultiSelect (popover + cmdk-поиск). Fallback на comma-separated TextInput
сохраняется когда target dict недоступен (scope-hide / 403 / 404), сервер
всё равно валидирует FK через ReferenceValidator.
Schema:
- spacecraft_codes → items.x-references: spacecraft.designator
- supported_format_codes → items.x-references: data_format.code
- supported_level_codes → items.x-references: processing_level.code
instrument schemaVersion 1.0.0 → 1.1.0, bundle 1.3.2 → 1.3.3 —
CuodBundleImporter перепишет schema_json в БД на старте writer'а,
admin-ui подхватит свежую схему через /api/v1/dictionaries/instrument.
Tests: 2 новых кейса в SchemaDrivenForm.test.tsx — happy path (MultiSelect
рендерится, запрос ушёл на правильный dict) + fallback (comma-separated
с подсказкой «список недоступен»). 171/171 passed.
instrument (Инструменты съёмки) — содержательный каталог полезной
нагрузки, кандидат на review-гейт наравне со spacecraft/ground_station.
На проде approvalRequired уже был true (историческое наследие, не из
бандла), на staging — false. Манифест его не помечал → рассинхрон.
Помечаем в manifest.json: importer при honorApprovalPolicy=true
применит флаг на staging без bump schemaVersion (CuodBundleImporter
строки 140-162). Bundle version 1.3.1 → 1.3.2.
mission у КА хранился свободным x-localized текстом (краткое описание),
дублировался, рассинхронизировался, не был связан со справочником
mission (где уже есть программы миссий + поле spacecraft_codes).
Теперь mission — строка-код, x-references → mission.code. Референсная
целостность + единый источник.
Маппинг 14 КА → 9 миссий взят из mission.spacecraft_codes (обратная
связь уже была в данных). Проверено: все 14 мапятся однозначно,
0 пропусков, 0 конфликтов.
- 0027-spacecraft-mission-fk.xml — Liquibase-миграция существующих БД:
схема + 14 записей одной транзакцией прямым SQL (смена типа поля —
chicken-and-egg, как 0020 raw SQL минует Envers/outbox; forward-only).
schema_version → 2.0.0.
- cuod-bundle: spacecraft.schema.json mission → x-references,
records/spacecraft.records.json — 14 mission → код, manifest 2.0.0.
Свежие установки получают новую форму сразу.
Caveat: миграция через raw SQL не пишет Envers _AUD и Kafka outbox —
проекции read-api нужно пересобрать после деплоя (как при 0020).
Approval Workflow v2 Phase 3 — soak prep:
- BundleManifest.DictionaryEntry: добавлены опциональные approvalRequired
(Boolean) и approvalMinRole (String).
- CuodBundleImporter: применяет policy на fresh + on-update + при стабильной
схеме (flip без bumping schema_version). Apply ТОЛЬКО когда явно изменился
→ не штормим UPDATE'ами.
- CuodBundleProperties.honorApprovalPolicy (default false): env-gated guard.
Bundle ship'ит approvalRequired:true для пилотных dicts, prod игнорирует
пока не явно включит.
- manifest cuod 1.3.0 → 1.3.1: spacecraft + ground_station получили
approvalRequired:true. Без honor flag — no-op.
Этого commit'а достаточно для соответствующего helm change в
ordinis-infra (bundleHonorApprovalPolicy=true в values-staging) — flip'ает
spacecraft/ground_station на staging при следующем deploy. Prod остаётся
default=false до окончания soak.
Orphan FK references в seed данных bundle v1.2.1 — references на codes которых
нет в target словарях. Bundle import не валидирует x-references (privileged
data load), поэтому records ингестились с broken FK. На prod такие orphans
видны через ReferenceValidator при попытке update + через OrphanReferenceScanner.
Найдено через scan по 6 FK relations (antenna, consumer, deliverable_type,
orbital_regime, spacecraft×2):
1. antenna.ground_station (4 orphans):
- GS_MOSCOW → MOSCOW_OTRADNOE (already существует)
- GS_KHANTY/KRASNOYARSK/KHABAROVSK → добавили новые ground_station records
(KHANTY_MANSIYSK_NPC, KRASNOYARSK_NPC, KHABAROVSK_NPC) — реальные станции
НИЦ ПД ОМЗ Роскосмоса.
2. deliverable_type.typical_processing_level (3 orphans):
- RADIANCE_L1B: L1 → L1B (businessKey hints L1B)
- ORTHO_L1C: L1 → L1C (businessKey hints L1C)
- SURFACE_REFL_L2A: L2 → L2A (businessKey hints L2A)
3. spacecraft.status — already исправлено в commit 5d5b03c (ACTIVE → OPERATIONAL).
На prod 4 records остались с ACTIVE из старого import — нужен manual PATCH
через writer API либо TRUNCATE + restart bundle import.
Verified: local seed scan теперь returns 0 orphans across all 6 FK relations.
Prod cleanup TODO (отдельный manual step, требует admin Keycloak token):
- spacecraft.status: 4 records (1999-068A, 2014-016A, 2015-028A, 2024-149E) → OPERATIONAL
- antenna.ground_station: 4 records → новые codes (MOSCOW_OTRADNOE / KHANTY_MANSIYSK_NPC / etc)
- deliverable_type.typical_processing_level: 3 records → L1B/L1C/L2A
Записи ground_station и similar dicts имеют lat/lon в data, но
geometryWkt не задан. Spatial filter (AOI) WHERE geometry IS NOT NULL
AND ST_Intersects(...) возвращал 0 records даже когда координаты
есть.
Три места починены:
1. DictionaryRecordService.resolveGeometry — для CRUD через REST.
Если req.geometryWkt() задан — приоритет (explicit). Иначе
читаем data.lat/lon (или latitude/longitude/lng), валидируем
ranges, строим POINT(lon lat) с SRID 4326. Игнор out-of-range
без throw — не ломаем legacy data.
2. CuodBundleImporter.deriveLatLonGeometry — bundle bypass'ит service,
reuse'аем ту же логику локально (5 строк inline). Применяется при
создании НОВЫХ records (existing skipped по businessKey).
3. Liquibase migration 0015-backfill-latlon-geometry — UPDATE existing
records где geometry IS NULL AND data has lat/lon. Три changeset'а
для разных aliases (lat/lon, latitude/longitude, lat/lng). Idempotent
(только NULL→value), rollback supported (NULL обратно).
После deploy: existing 3 ground_station записей (Отрадное и др.) и
spacecraft с lat/lon получат POINT geometry. AOI bbox-фильтр заработает.
Bundle v1.2.1 ввёл x-references на spacecraft_status, но seed records
оставались со старыми enum-значениями (ACTIVE, DECAYED) которых нет
в spacecraft_status.records.json.
Migration:
- ACTIVE (12 records) → OPERATIONAL (штатная целевая работа)
- DECAYED (2 records) → LOST (аварийная потеря или внеплановый сход)
Mapping выбран по семантике: ACTIVE = currently working = OPERATIONAL.
DECAYED означает unplanned re-entry = LOST (DECOMMISSIONED это плановый
вывод). После следующего bundle re-import (TRUNCATE+restart) seed
будет проходить x-references validation.
Был inline enum [PLANNED, ACTIVE, INACTIVE, DECAYED, LOST] — но
spacecraft_status справочник существует с нормализованными codes:
PLANNED, LAUNCHED, OPERATIONAL, STANDBY, DEGRADED, DECOMMISSIONED, LOST.
Теперь spacecraft.status = x-references на spacecraft_status.code.
Profit:
- i18n названий статусов (Запланирован/Эксплуатация/...)
- description per status (что значит DEGRADED vs DECOMMISSIONED)
- Расширяемо без schema migration
Breaking change: existing 14 records со status=ACTIVE/DECAYED не пройдут
validation. Решение: full re-import bundle на staging (TRUNCATE +
restart ordinis-app → CuodBundleStartupRunner пересоздаст все).
Демо нового x-references validator (commit 4142678).
До этого FK relationships были только в description как "FK на ...".
Теперь app-level integrity check на CRUD:
- spacecraft.satellite_type_code → satellite_type.code
- consumer.consumer_type → consumer_type.code
- deliverable_type.typical_processing_level → processing_level.code
- orbital_regime.parent_orbit_type → orbit_type.code
- antenna.ground_station → ground_station.code
Effect: POST/PUT записи с invalid FK → 422 ValidationError code
x_references_target_not_found, не silent broken reference.
Bundle version bumped 1.2.0 → 1.2.1 (breaking-change detection
прокатит — schemaVersion полей не менялись, только metadata).
Альтум-команда обнаружила что /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, +МСУ-ГСМ Арктики)
Importer бросал early-return для уже существующих definitions, поэтому
records никогда не наполнялись на повторных стартах. Records seed
теперь вызывается всегда (idempotent — skip по businessKey).
CuodBundleImporter теперь после создания DictionaryDefinition подгружает
records/<name>.records.json (relative to bundle resourceRoot) и создаёт
DictionaryRecord для каждой записи. Идемпотентно: пропускает business_key,
который уже существует.
Bundle:
- 5 satellite_type (SAR_X_HIRES, OPT_HIRES_VIS, OPT_MIDRES_MS, HYPER, METEO_GEO)
- 4 spacecraft (Terra, Sentinel-1A, Sentinel-2A, Кондор-ФКА №2)
- 3 ground_station (Отрадное, SvalSat, Байконур)
Все с локализованными name/description (ru-RU + en-US), валидируемые
JSON Schema на pre-save. Outbox events RecordCreated отправляются для
projection-writer.