229 Commits

Author SHA1 Message Date
emelianov fe54c095ab Формат времени в UTC 2026-06-25 16:21:17 +03:00
emelianov d0ec0ef754 считывание точки прицеливания с карты 2026-06-23 17:13:30 +03:00
emelianov 1e8b9f6998 центрирование каты при тестовом расчете ПУУД 2026-06-18 12:13:50 +03:00
emelianov f0f39e784e Merge remote-tracking branch 'origin/dev' into dev 2026-06-17 15:28:09 +03:00
emelianov d38822db3e Расчет витков по TLE 2026-06-17 15:28:04 +03:00
Дмитрий Соловьев e015b8aa75 Merge branch 'feat/tgu-slots-filters' into 'dev'
fix(tgu-ui): таблица слотов — слить виток/цикл, секунды в начале, длительность

See merge request 2-6/2-6-4/missionis/observatio-terrae!3
2026-06-17 11:17:08 +00:00
Дмитрий Соловьев 354730fb6c feat(tgu-ui): запрет выбора конфликтных слотов + прохождение без подсветки
- конфликтные слоты нельзя добавить в выбор под бронь (клик по строке,
  Shift-диапазон, выбор полосы на карте, «выбрать все») — жёсткая блокировка;
  снятие выбора не блокируется (конфликт считается только для невыбранных)
- слоты одной полосы между собой не конфликтуют (groupOf) — иначе нельзя было
  бы набрать полосу поштучным кликом
- чекбокс «Конфликты» теперь и гасит, и блокирует выбор; выкл. — выбирать любые
- переход на прохождение (◀/▶) больше не подсвечивает никакой слот: активная
  подсветка строки — только по явному клику; добавлена прокрутка к началу прохода
- повторный Shift+клик по тому же концу снимает выделение (таблица и карта)
2026-06-17 14:07:03 +03:00
Дмитрий Соловьев c402f376c5 feat(tgu-ui): степпер по прохождениям и подсветка конфликтов выбора слотов
Степпер прохождений (виток как проход над районом):
- slotPasses.ts: группировка слотов в проходы (КА·cycle·revolution·sign) в
  хронологии по всем КА + юнит-тесты;
- PassStepper.tsx: ◀/▶ листают проходы, окно суток следует за курсором,
  таблица скроллится к проходу, карта не прыгает; выбранный проход
  изолирует отображение, «Все» — назад ко всему набору;
- дата/время прохождения отдельной строкой во всю ширину (видно целиком).

Подсветка конфликтов выбора (мягкая подсказка «станет недоступным»):
- slotConflicts.ts: тот же КА + недостаточный зазор на манёвр — 10 с при том
  же крене, 30 с при смене крена (ROLL_EPSILON); конфликтные слоты гаснут в
  таблице и на карте, остаются выбираемыми (жёсткая выполнимость — за бэком);
- производительность: оконный поиск по времени (~O(N·logM)), кандидаты —
  только видимые, выбор через useDeferredValue (не молотит во время drag);
- чекбокс «Гасить конфликты» в строке выбора: выключает и слой, и расчёт;
- юнит-тесты на правило зазора и оконный поиск.

Карта: проп dimmedObjectIds — серый «налёт» поверх конфликтных слотов в SVG
overlay (без изменения canvas).
2026-06-17 13:31:28 +03:00
Дмитрий Соловьев aabb550a0a fix(tgu-ui): бронь не сбрасывает окно фильтра по суткам
Слайдер «Сутки» раскрывался на весь диапазон после брони: bookedSlots менялся → новый референс days → эффект сбрасывал окно, хотя набор суток тот же. Завязал сброс на сигнатуру набора суток (границы дней), а не на референс массива.
2026-06-17 12:11:03 +03:00
Дмитрий Соловьев 653c930dd6 feat(tgu-ui): выбор полосы слотов мышью на карте (Shift+drag) 2026-06-17 11:31:21 +03:00
Дмитрий Соловьев 61a2c92d28 feat(tgu-ui): Shift+клик ближе к якорю сжимает выделение полосы 2026-06-17 10:20:19 +03:00
Дмитрий Соловьев b092e50dd8 feat(tgu-ui): выбор полосы слотов мышью «от первого к последнему»
Замена авто-чипа «полоса ×N» на ручной жест. Полоса переопределена:
один КА + один крен (ε) + подряд по времени; виток в границу не входит.

- slotStrips.ts: groupStrips → { idByKey, membersOf }; группировка по КА →
  кластеры по крену (ROLL_EPSILON=0.5) → деление по каденции (CADENCE_GAP_FACTOR=1.5)
- slotSelection.ts: selectStripRange вместо toggleStripSelection
- TguSlotsTab.tsx: клик по строке = якорь + выбор; Shift+клик тянет полосу
  между якорем и строкой; выход за полосу запрещён + подсказка; рельс полосы якоря
- tgu-planning.css: убраны стили чипа/hover; user-select:none на строках
- тесты обновлены (125 зелёных), tsc/build чисто
2026-06-17 10:13:11 +03:00
Дмитрий Соловьев 8faaa03af3 chore: скрипты сборки всех сервисов + gitignore артефактов
- buildAll.sh: ./gradlew bootJar всех Spring-сервисов (--clean опц.).
- serviceAllBuild.sh: раскладка bootJar + config-repo + run-хелпер в serviceAllBuild/.
- .gitignore: serviceAllBuild/ (артефакты ~1.7G) и локальный .npmrc фронта.
2026-06-17 09:06:43 +03:00
Дмитрий Соловьев 7c10f9a10d docs: бэклог #49–#53 + дизайн-план выбора слотов полосами
- бэклог-pcp-tgu-ops-ui: #50 (M1/M2/M3 слой поддержки решений), #51 (вызовы через
  gateway), #52/#53 (находки /review), #49 (баг обновления BUSY после снятия брони),
  раздел «Выполнено» (чистки по /review).
- docs/tasks/TASK_slot-strip-selection: дизайн-план и артефакты /autoplan по выбору
  полосами; отметка о реализации it.1.
2026-06-17 09:06:35 +03:00
Дмитрий Соловьев 1f44c1a5bb feat(tgu-ui): #50 слой поддержки решений по слотам + выбор полосами
Поверх ручного бронирования — информационный слой (достаточность судит оператор).

M1 покрытие: coverage.ts (растровое Monte-Carlo % покрытия заявки, чистые функции
  + coverage.test.ts), CoveragePanel (живой % + бар), слой coverage на карте
  (drawCoverage, union контуров, клип по заявке).
M2 эргономика: сортировка списка по вкладу в покрытие (жадный маргинальный прирост),
  по крену; колонка «Вклад» (+N%).
M3 конфликты: бейдж держателя BUSY-слота (заявка + важность) из holders в SlotDTO.

Выбор полосами (TASK_slot-strip-selection it.1): slotStrips.ts (groupStrips —
  same-roll цепочки по-КА, ε=0.01 с якорным креном, тотальный порядок) +
  slotStrips.test.ts; toggleStripSelection в slotSelection.ts; чип «полоса ×N» в
  строке, рельс по StripId в сортировке «время», подсветка полосы при наведении.
  Физику выполнимости не дублируем на фронте — это backend validate (it.2).

Смежный баг: anchorIndexRef (якорь Shift-выбора) сбрасывается при смене видимого
набора (фильтры/окно суток), не только сортировки.
2026-06-17 09:06:25 +03:00
Дмитрий Соловьев 20d9eb9579 chore(config): #51 slots-service ходит к чужим API строго через gateway
- pcp-gateway-service: добавлен маршрут satellite-catalog
  (/api/pcp-satellite-catalog/** -> StripPrefix=2).
- slots-service: settings.request-service/earth-grid-service/satellite-catalog-service
  переведены на ${pcp.services.gateway}/api/pcp-<x> (код строит $base/<path>,
  gateway снимает префикс). Прямые адреса сервисов больше не используются.
2026-06-17 09:06:04 +03:00
Дмитрий Соловьев bd35280691 feat(slots-service): #50 M3 — держатели BUSY-слотов в request-cover
reqCover(all=true) обогащает занятые (BUSY) слоты держателями: какие заявки
блокируют слот, с именем и важностью (обзор конфликтов при ручной броне).

- SlotHolderDTO (requestId + опц. name/importance), поле holders в SlotDTO
  (null для AVAILABLE/BOOKED и прочих путей — обратно совместимо).
- TestSatelliteImpl: для BUSY-слота собирает держателей по пересечению
  сдвинутых на cycle интервалов активных броней (та же логика, что band, с mmi),
  distinct по requestId (множественный держатель).
- EarthService.requestHolders: батч-резолв name/importance заявок; деградация
  через runCatching → emptyMap (request-service недоступен → только id, без падения).
- SlotService.enrichHolders: один батч-запрос на обзор, slot.copy(holders=...).
- SlotDTO переведён в data class (copy вместо ручного перечисления полей).
- Тест: BUSY несёт держателей, BOOKED/AVAILABLE — нет.
2026-06-17 09:05:52 +03:00
emelianov 6938700218 исправлен расчет витка для tle 2026-06-16 12:26:24 +03:00
Дмитрий Соловьев 5869994690 feat(tgu-slots): подтверждение брони всегда + «Снять всё» снимает всю заявку
- Бронь теперь всегда через диалог подтверждения (симметрично «Снять всё»),
  убран порог в 500 слотов.
- «Снять всё» дочитывает брони заявки через by-request и снимает объединение
  локального и серверного наборов bookedSlotId — гарантированно вся заявка.
- Карта: забронированные слоты вне окна суток подчинены фильтрам видимости
  (статус «бронь»/КА/ветвь) — при выключенном фильтре брони их не видно.
2026-06-15 17:22:35 +03:00
Дмитрий Соловьев 3c94815ffc fix(slots-service): #48 by-request отдаёт бронь в базовом cycle-фрейме
GET booking/by-request/{id} возвращал tn/tk/revolution из базового
cycle-фрейма без сдвига (BookedSlotEntity.toViewDTO), тогда как все
остальные пути (toDetails, allByInterval→shiftedToCycle,
resolveBookedSlotStartTime) сдвигают на cycle*durationCalc и
cycle*cycleRevs. Для броней с cycle > 0 дата уходила на cycle*durationCalc
суток раньше расчётной (cycle=0 совпадал, поэтому баг прятался; #42
починил только ключ sat:slotNum:cycle, не время).

Сдвиг перенесён в SlotService.bookedByReqId через новый
toShiftedViewDTO(satellite); КА резолвится из каталога, нет в каталоге —
базовый фрейм без падения списка. Небезопасный базовый билдер в сущности
удалён. Контур/координаты не сдвигаются (наземный след повторяется
каждый цикл), симметрично shiftedToCycle.

Регрессия в SlotServiceCancelTest: сдвиг tn/tk/витка на cycle брони и
fallback без КА. Бэклог #48 помечен сделанным.
2026-06-15 17:22:25 +03:00
Дмитрий Соловьев b38a97b9a5 feat(tgu-ui): слоты — панель фильтров, показ всех слотов и статус «недоступен»
- Панель фильтров видимости (статус/КА/ветвь) над строкой выбора: единый
  filteredSlots сужает и таблицу, и карту; пустое состояние с «Сбросить».
- Убран потолок отрисовки карты — общую картину не режем, объём сужает оператор
  фильтрами.
- Карту не перецентрируем после расчёта (был прыжок на произвольный первый слот).
- «Показать все слоты» шлёт all=true и показывает занятые бронью (BUSY); бронь
  их не берёт (исключены из выбора/счётчика, чекбокс disabled).
- Починен мёртвый маппинг статуса: бэкенд шлёт BUSY, фронт ждал OCCUPIED.
- BUSY отображается как «недоступен» (конфликт с бронью), а не «занят» —
  чтобы не путать с самой бронью.
2026-06-15 16:45:28 +03:00
Дмитрий Соловьев 052ed5f04b feat(slots-service): параметр all у request-cover — возврат занятых бронью (BUSY) слотов
«Показать все слоты» должно показывать полный набор. При all=true reqCover
запрашивает статусы [AVAILABLE, BOOKED, BUSY], а selectCoverageSlots собирает
BUSY отдельно и добавляет в ответ только в ветке без покрытия — в solver
(«Подобрать покрытие») занятые слоты по-прежнему не попадают.
2026-06-15 16:45:14 +03:00
emelianov 1790192e06 Проверка на пересечение ЛВ Земли при расчете ПУУД 2026-06-15 16:44:51 +03:00
emelianov ee68da30f2 Проверка на пересечение ЛВ Земли при расчете ПУУД 2026-06-15 16:23:27 +03:00
emelianov f49709d544 Расчет с выравниванием СДИ 2026-06-15 16:05:45 +03:00
emelianov 88fb380be4 Контур по параметрам съемочного устройства 2026-06-15 15:38:09 +03:00
emelianov 60ddec0928 fix в задании азимута 2026-06-15 15:11:20 +03:00
Дмитрий Соловьев bffb8e47b7 fix(tgu-ui): двуручейный слайдер — выбор ползунка по направлению при наложении
При наложении в крайней точке хват попадал на верхний ползунок to, который на
правом краю заперт fromIndex и не уходит влево, а нижний from был недоступен.
Теперь при fromIndex === toIndex управляемый ползунок выбирается по направлению
движения (влево → from, вправо → to). Чинит и «Сутки» (SlotDaySelector), и
«Витки» (MapPassSelector).
2026-06-15 14:38:34 +03:00
Дмитрий Соловьев 33b56ba951 feat(slots): снятие брони пачками с прогрессом (симметрично бронированию)
Бэк (slots-service):
- DELETE /booking/batch (cancelReqSlots) — снятие выбранных броней пачкой;
  одиночный DELETE теперь request-aware (нужен requestId), чтобы шаренный
  между заявками слот рвал только связь своей заявки, а не сносил бронь.
- BookedSlotViewDTO (+toViewDTO) с cycle БРОНИ — slotKey совпадает с ответом
  POST booking/request; GET booking/by-request отдаёт View.

Фронт (pcp-tgu-ui-service):
- runChunked + ChunkProgress: бронь и снятие идут пачками по 200 с общим
  прогресс-баром (стиль/позиция как у оверлея «Расчёт слотов…», сверху).
- «Снять выбранные», «Снять всё» и ✕ в строке — единый чанковый движок
  runCancel (а не bulk-запрос), брони слиты в строки таблицы и выбираемы.

Тесты: SlotServiceCancelTest (шаринг/единоличная/cycle-регрессия), runChunked,
slotsApi.cancelBookedSlots/fetchBookedSlotsByRequest.
2026-06-15 14:26:06 +03:00
emelianov 29b8206867 Контур для ПУУД 2026-06-15 12:49:45 +03:00
emelianov 7fdac8c71e Прототип страницы расчета ПУУД 2026-06-15 12:19:53 +03:00
emelianov 07ba650e03 Прототип страницы расчета ПУУД 2026-06-15 11:58:49 +03:00
Дмитрий Соловьев b9adbce207 style(design): FINDING-003 — занятый слот приглушённо-красным бейджем (статусы сканируются) 2026-06-15 11:45:51 +03:00
Дмитрий Соловьев 1c9cdf4ad2 style(design): FINDING-002 — заголовки таблицы слотов на --text-muted (контраст AA) 2026-06-15 11:43:36 +03:00
Дмитрий Соловьев 6db196066d style(design): FINDING-001 — числовые колонки слотов (Длит./Крен) по правому краю + tabular-nums 2026-06-15 11:43:00 +03:00
Дмитрий Соловьев 7f9cd34ae9 build(tgu-ui): lockfile для @tanstack/react-virtual; игнорировать requests.json (скретч) 2026-06-15 11:35:16 +03:00
Дмитрий Соловьев a5f3f0e375 fix(tgu-ui): таблица слотов — слить виток/цикл, секунды в начале, длительность
По замечаниям: Крен и Статус не помещались (8 колонок на узкую панель).
- «Виток» теперь = виток / ветвь / цикл (281 / восх / 1); отдельная «Слот/цикл» убрана
- «Начало» с секундами (formatLocalDateTimeSeconds) — слоты короткие, без секунд начало=конец
- «Конец» заменён на «Длит., с» (slotDurationSec); colSpan распорок 8→7
- Крен и Статус снова видны

Тесты 70/70, build чисто.
2026-06-15 11:10:49 +03:00
Дмитрий Соловьев 3b1a13e532 chore: игнорировать корневой package-lock.json (артефакт npm без package.json) 2026-06-15 10:29:57 +03:00
Дмитрий Соловьев 11416de7aa config: локальный адрес Camunda gRPC localhost -> 192.168.60.201:26500 2026-06-15 10:25:29 +03:00
emelianov 3ab5b8860f Merge remote-tracking branch 'origin/dev' into dev 2026-06-15 10:13:33 +03:00
emelianov ce32a03072 Прототип страницы расчета ПУУД 2026-06-15 10:13:27 +03:00
Дмитрий Соловьев 8c18959c6b build: отключить недоступный maven-proxy (repo.nstart.cloud отдаёт HTML), плагины с публичных репозиториев 2026-06-15 10:06:03 +03:00
Дмитрий Соловьев a28dd62506 Merge remote-tracking branch 'origin/dev' into dev 2026-06-15 10:05:56 +03:00
emelianov bf2e88431e ПУУД с постоянной ориентацией 2026-06-15 09:24:52 +03:00
Дмитрий Соловьев c64222a90d feat(tgu-ui): бронирование слотов — виртуализация, автоподбор покрытия, эргономичный мультивыбор
Вкладка «Слоты» масштабирована под ~20k слотов/мес без торможения:
- wktCache: контур слота парсится один раз (кэш по slotKey), не на каждый ререндер сцены
- виртуализация таблицы (@tanstack/react-virtual): в DOM только видимое окно строк
- автоподбор покрытия: проброс coverageStrategy (GREEDY/CONTINUOUS) в request-cover,
  результат — преднабор брони (solver не дублируется, используется бэкендный)
- мультивыбор: Shift-диапазон по отсортированному списку, «все сутки»/«видимые»,
  счётчик охвата выбора
- бронь крупных наборов чанками (по 200) + подтверждение выше 500 + прогресс
- отмена расчёта (AbortController), мягкая подсказка перегруза карты
- чистая логика выбора/чанкинга вынесена в slotSelection.ts + юнит-тесты

Тесты: 70/70 (17 новых). tsc -b и vite build чисто.
2026-06-11 17:09:58 +03:00
Дмитрий Соловьев 345529cde0 chore: сохранение текущих изменений (deploy local-bundle/local-jar, request-service, slots UI markedObjectIds)
Безопасный коммит незакоммиченной работы перед реализацией бронирования слотов.
Вне коммита оставлены: package-lock.json (пустой, мусор), requests.json (scratch-данные).
2026-06-11 14:00:12 +03:00
Дмитрий Соловьев 6689351ef0 feat(tgu-ui): вкладка «Слоты» — расчёт покрытия и бронирование слотов заявки
- новая вкладка: заявки интервала на карте, выбор заявки кликом/инспектором,
  расчёт слотов покрытия, бронирование и снятие брони
- выбор заявки без выпадающего списка (не масштабируется на ~10к): все заявки
  на карте, панель справа с кнопкой «Выбрать», имя выбранной заявки в тулбаре,
  кнопка «Сброс», фильтр заявок по интервалу
- бронь отмечаем по ответу POST booking/request (by-request отдаёт слот в чужом
  cycle-фрейме — ключ не совпадал, бейдж «бронь» не появлялся)
- подсветка и автоскролл активной строки по клику на слот (ручной scrollBy)
- меню выбора при плотном наложении слотов (SlotPickMenu); контур заявки
  pickable:false, чтобы не «съедать» меню; панель не закрывается при выборе
- фильтр отображения по суткам (SlotDaySelector)
- gateway: маршрут slots (/api/pcp-slots/**)
- карточка полосы обзора (swath): КА и виток в инспекторе
2026-06-11 12:20:28 +03:00
Дмитрий Соловьев 802c966347 fix(tgu-ui): подхватывать существующий черновик вместо ошибки «Conflict» при добавлении съёмки
При первой правке плана ensureDraft→createDraft(replace) падал 409, если по КА
уже есть открытый черновик на это окно (осиротевший с прошлой сессии — авто-отмена
best-effort). В UI это показывалось голым «Conflict» (на стенде не подхвачен
include-message у tgu).

Теперь на 409 от createDraft редактор подхватывает существующий черновик того же
КА с тем же окном (adoptExistingDraft через fetchDraftPlans, сверка окна ±2 c) и
продолжает в нём; если подходящего нет — читаемое сообщение со ссылкой на вкладку
«Планы» вместо «Conflict».
2026-06-10 16:13:59 +03:00
Дмитрий Соловьев 7202f7cb02 refactor(tgu-ui): единый инспектор объектов карты (MapObjectInspector) на вкладках «Карта» и «Комплексный план»
Обобщил ComplexInfoPanel в общий MapObjectInspector (tgu-map-2d): карточки для
всех типов объектов — заявка, включение комплексного плана, ячейка тепловой карты,
маршрут planWorks, станция, маркер КА, линия; список с раскрытием при наложении.

Вкладка «Карта» переведена на enableObjectChooser + onObjectsAtPoint + правый
инспектор — заменила разрозненные Tgu2DMapRequestsPanel/Tgu2DMapStationPanel (удалены)
и объектную часть нижней плашки (Tgu2DMapDetails ужат до контекста плана). Вкладка
«Комплексный план» использует тот же инспектор.

RequestAtPoint переехал в tgu-requests/RequestDetailsPanel (нужен вкладке «Заявки»).
2026-06-10 15:58:55 +03:00
Дмитрий Соловьев bdbc874f41 feat(tgu-ui): тепловая карта приоритетов и инспектор объектов на вкладке «Комплексный план»
Чекбокс «Тепловая карта» — фоновый слой-заливка ячеек сетки заданий по важности
(cool→hot), источник GET /api/pcp-request/v1/cells/priority-map (cellsApi + drawHeatmap).
Heatmap — полноценный слой MapPolygon (layer "heatmap", importance/cellNum), участвует
в spatial-индексе и chooser, рисуется под контурами плана/заявок.

Клик по объекту открывает правый инспектор ComplexInfoPanel (по аналогии с редактором):
карточки ячейки приоритета, включения комплексного плана и заявки; при наложении —
список объектов под точкой клика с раскрытием. При включённой тепловой карте ячейка
всегда идёт первой в списке и раскрыта по умолчанию.

Бэклог #44 отмечен сделанным.
2026-06-10 15:47:18 +03:00
Дмитрий Соловьев 11380fced5 fix(tgu-ui): гасить «Создать черновик» для стартовавшего плана (replace→422)
Кнопка предлагала replace-черновик для уже стартовавшего плана, хотя бэк
(CreateDraftUseCase.createReplaceDraft) возвращает 422 «окно выдачи заморожено».
Гард canCreateDraftFromPlan теперь требует startInFuture (зеркалит редактор);
для стартовавших eligible-планов вместо кнопки — amber-подсказка про fork-forward.
2026-06-10 15:46:57 +03:00
Дмитрий Соловьев 3765efb4fe feat(ui): перевести фронт с pcp-ui-service на источники, снять gateway catch-all
Запросы tguApi.ts (platforms/plans/decision/dismiss/restore/confirm-layin)
и flight-line переведены с прокси /api/tgu-planning через умирающий
pcp-ui-service на прямые вызовы pcp-tgu-service (/api/pcp-tgu, decision и
confirm-layin — на тест-контроллер /test/tgu/*) и pcp-ballistics-service.
flight-line перенесён в ballisticsApi.ts. editorApi.ts modes переведён с
/api/current-plans на /api/pcp-mission. UX-перевод ошибок решения уже жил во
фронте (errorMessage в tguApi.ts), серверная обвязка ui-service дублировала.

Из config-repo/pcp-gateway-service.yaml снят маршрут ui-catch-all: не-матчнутый
/api/** теперь отдаёт 404 на gateway, а не уходит в pcp-ui-service.

Шаг 8 (вывод сервиса из эксплуатации: compose/helm/CI/реестр + удаление кода)
отложен до проверки репойнта на живом стенде; сервис пока задеплоен без
маршрутов. См. docs/план-перевода-с-pcp-ui-service.md и задачу #40 бэклога.
2026-06-10 14:47:05 +03:00
Дмитрий Соловьев 3c4ebcd7d3 refactor(cors): централизовать CORS на gateway, убрать per-service из pcp-ui-service
CORS-политика (allowedOriginPatterns:* + allowCredentials) переезжает в единый
globalcors на pcp-gateway-service. После сворачивания фронта на gateway браузер
ходит к бэкендам только через него (same-origin via Vite/nginx), поэтому
per-service CORS в pcp-ui-service/WebConfig.kt стал мёртвым грузом. globalcors
оставлен как страховка от cross-origin напрямую к gateway.
2026-06-10 14:04:29 +03:00
Дмитрий Соловьев ab3a787da5 feat(gateway): сервис pcp-gateway-service + сворачивание маршрутизации фронта на него
- новый Spring Cloud Gateway (WebFlux): доменные маршруты /api/pcp-<x>/**
  со StripPrefix=2, /api/stations без среза пути, catch-all /api/** → ui-service;
  опциональная JWT-валидация на edge (gateway.security.jwt-enabled, по умолчанию off)
- config-repo: pcp-gateway-service.yaml (маршруты) + порт/адрес gateway в реестрах
  application-{local,docker-local,dev}
- build: репозитории переведены на mavenCentral (nstart-proxy закомментирован),
  включены mavenLocal/gradlePluginPortal; settings: include модуля
- pcp-tgu-ui-service: vite-proxy и nginx.conf свёрнуты на единый /api → gateway
  (вместо набора per-service правил)
- deploy: сервис gateway в docker-compose; helm-чарт pcp-gateway-service
- CI: .gitlab/ci/pcp-gateway-service.yml + проводка в .gitlab-ci.yml
2026-06-10 13:34:16 +03:00
emelianov 8044815e97 Merge remote-tracking branch 'origin/dev' into dev 2026-06-10 13:33:11 +03:00
emelianov 6ddafa3dd2 ПУУД с постоянной ориентацией 2026-06-10 13:33:07 +03:00
Дмитрий Соловьев 6c2491cbb9 ci(tgu): образ gradle:jdk21-ubi10 вместо eclipse-temurin (для gradle в PATH) 2026-06-10 09:33:42 +03:00
Дмитрий Соловьев cf14880309 ci: использовать gradle из образа вместо ./gradlew (убран chmod +x gradlew) 2026-06-10 09:14:00 +03:00
Дмитрий Соловьев 9206ef6c49 tle-monitoring: datasource из env (config dev + helm-env БД pcp_tle, секрет pcp-tle-db) 2026-06-09 17:59:26 +03:00
Дмитрий Соловьев 35b8afc3fe helm(dynamic-plan): секрет БД pcp-complex-plan-db в соответствии с именем БД 2026-06-09 17:57:48 +03:00
Дмитрий Соловьев 632ef7f141 config(dev): datasource из env для всех сервисов; helm-env БД для dynamic-plan и tgu
Откат регрессии 0ec8c30: в профиле dev datasource снова берётся из
SPRING_DATASOURCE_URL/USERNAME/PASSWORD (профиль local оставлен параметрическим).
dynamic-plan и tgu переведены на тот же механизм + добавлены helm extraEnv
с in-cluster postgres и кредами из секретов pcp-dynamic-plan-db / pcp-tgu-db.
2026-06-09 17:40:09 +03:00
Дмитрий Соловьев c07fce093e config-server: git-репозиторий и деплой на observatio-terrae (ветка dev, deploy-token-5) 2026-06-09 17:19:51 +03:00
Дмитрий Соловьев ccbb77787f config-server(dev): переключение на репозиторий observatio-terrae, ветка dev 2026-06-09 17:08:40 +03:00
Дмитрий Соловьев c2a847fc2e ci: рубильник RUN_ALL для прогона всех gradle-сервисов (кроме pcp-tgu-ui-service)
Run pipeline + RUN_ALL=true прогоняет test/build/package всех бэкендов
независимо от changes; deploy:dev остаётся ручным. Обычный пуш по-прежнему
собирает только изменившийся сервис. pcp-tgu-ui-service source-guard-нут
(только push), чтобы не попадать в RUN_ALL/web-пайплайны.
2026-06-09 14:55:04 +03:00
Дмитрий Соловьев f11e143a9c ci: триггер ребилда всех gradle-сервисов (кроме pcp-tgu-ui-service) 2026-06-09 14:08:09 +03:00
Дмитрий Соловьев e51c7965ec config(pcp-request-service): вернуть env-плейсхолдеры datasource в shared-профиль
Merge 0ec8c30 затёр ${SPRING_DATASOURCE_URL/USERNAME/PASSWORD} на конкретный
postgres в shared-профиле, из-за чего падал тест RequestServiceConfigTest
(shared datasource keeps environment placeholders) и красился CI.
Плейсхолдеры восстановлены; local-профиль остаётся с конкретным postgres.
2026-06-09 14:00:24 +03:00
Дмитрий Соловьев cb439319c5 pcp-tgu-ui-service в группе сервисов pcp: регистрация в CI + per-service Node-пайплайн + helm-чарт 2026-06-09 14:00:24 +03:00
Дмитрий Соловьев f292291e04 Удаление старого pcp-tgu-ops-ui: заменён на pcp-tgu-ui-service 2026-06-09 14:00:24 +03:00
Дмитрий Соловьев 505bf1f31c Новый сервис pcp-tgu-ui-service: ТГУ-фронт на headless-auth nstart + платформенный CI/деплой 2026-06-09 14:00:24 +03:00
emelianov 0b617594fd Merge remote-tracking branch 'origin/dev' into dev 2026-06-09 13:41:54 +03:00
emelianov e6822b6caf ПУУД с постоянным азимутом 2026-06-09 13:41:50 +03:00
Дмитрий Соловьев a5c6b66826 CI: перевод сборки на gradle jdk21-ubi10 2026-06-09 12:43:35 +03:00
Дмитрий Соловьев 060bcce9f2 Edit pcp-ballistics-service.yml перевод на java 21 2026-06-09 09:33:18 +00:00
emelianov faf88eaf94 ПУУД с постоянным азимутом 2026-06-09 12:31:53 +03:00
emelianov e1e96b1ff0 Merge remote-tracking branch 'origin/dev' into dev 2026-06-08 18:43:11 +03:00
emelianov 69f2d6ad4f ПУУД с постоянной ориентацией 2026-06-08 18:43:05 +03:00
Дмитрий Соловьев 2a04a81bc0 Merge branch 'tgu-ops-ui' into 'dev'
Сервис pgu-ops-ui

See merge request 2-6/2-6-4/missionis/observatio-terrae!2
2026-06-08 11:58:27 +00:00
Дмитрий Соловьев 8ac4afa195 config: адреса инфраструктуры на localhost (application-dev/docker-local, create_pcp_databases) 2026-06-08 14:45:50 +03:00
Дмитрий Соловьев 1a0348cdeb merge: влить dev (ПУУД, angular-motion-lib, fix requests) в tgu-ops-ui 2026-06-08 14:25:29 +03:00
Дмитрий Соловьев 5fa3ceaac9 feat(группировка): реальная съёмка/сброс/связь и заполнение борта
Дашборд «Группировка» наполняется из плана: режимы SURVEY/DROP → интервалы
съёмки и сброса с контуром и станцией, окна ЗРВ выделены в отдельную «связь»
(раньше показывались как сброс). Заполнение бортового накопителя считается
из объёмов маршрутов (volumeMb) с модельной ёмкостью 500 ГБ
(ONBOARD_CAPACITY_MB) — memAt по таймлайну, подсветка контура/линии сброса
на карте.
2026-06-08 14:00:03 +03:00
Дмитрий Соловьев 2c8b317c42 fix(editor): зажим крена маршрута к ±45°
Бэкенд считает геометрию видимости/маршрута только в |крен| ≤ 45°,
поэтому предел снижен с 89° до 45°. Введён общий clampRoll (зажим +
округление до 0.1°), применён при ручной правке крена (кнопки ±,
прямой ввод) и при загрузке сохранённого маршрута в окно построения
(легаси-маршрут вне ±45° зажимается).
2026-06-08 13:59:53 +03:00
Дмитрий Соловьев ff0b2639c9 feat(editor-map): отдельный цвет линии сброса на карте
Сброс рисуется приглушённо-малиновым (rgba(214,96,138,0.82), ширина 5)
поверх трассы: контрастно к бирюзовым виткам, не сливается с заявками
и контурами съёмки.
2026-06-08 13:59:46 +03:00
Дмитрий Соловьев c74327011a docs(бэклог): #39 — авторасчёт выдаёт маршруты с креном >45° (slots/каталог)
Зафиксирована диагностика: корень — оптические полосы Emissio в каталоге
(satellite_slot_angle) уходят до 50.42°, slots-service берёт центр полосы
без зажима по 45°. Версия «логика локации» проверена и отклонена.
2026-06-08 13:55:01 +03:00
Дмитрий Соловьев c1259c3105 pcp-request-service: аудит-фиксы RS1/RS3/RS6/RS7
RS1 (#29): планировщик истечения заявок — RequestExpiryService
(@Scheduled + FOR UPDATE SKIP LOCKED, мультиинстанс-safe), refreshStatus
вызывается и при создании заявки.
RS7 (#35): единый топик pcp.request.status.v1 → geoportal (COMPLETED/EXPIRED
в payload), замена orphan pcp.request.completed.v1; эмиссия из matching и
expiry через тот же outbox; backoff/next_attempt_at/cap для outbox FAILED.
RS3 (#31): DefaultErrorHandler + DeadLetterPublishingRecoverer
(DLQ pcp.route.georeference-dlq.v1), bounded retry, контрактные ошибки в DLQ.
RS6 (#34): проброс coverageRequiredPercent из API, разведение терминов
покрытие/важность.

Доки: бэклог, аудит сервиса заданий, карты взаимодействий.
RS2 (#30) отложено до #28, RS5 (#33) — до роста требований к точности.
2026-06-06 23:47:19 +03:00
Дмитрий Соловьев 2499aebf2d routes: объём маршрута (volumeMb) — расчёт, хранение в БД и отображение
Объём режима = скорость × длительность, Мбайт: съёмка 120 Мбайт/с (12 ГБ / 100 с), сброс — канал 1200 Мбит/с ÷ 8. Единый источник — ModeDataRates в pcp-types-lib. Хранится колонкой volume_mb (Flyway V10 mission-planing, V9 complex-mission, бэкфилл из duration), заполняется на всех путях построения через @PrePersist/@PreUpdate. DTO отдают вычисляемое read-only поле (та же формула). Фронт pcp-tgu-ops-ui показывает объём в редакторе (инспектор, построенный маршрут) и в сводке комплексного плана.
2026-06-06 18:34:29 +03:00
Дмитрий Соловьев 316e0ee552 pcp-tgu: эндпоинт /test/tgu/platforms/load-manual принимает JSON
Тестовый эндпоинт загрузки платформ принимает root-array НСИ в теле запроса (@RequestBody List<NsiPlatformDto>) вместо пути к файлу; маршрут load-from-file переименован в load-manual.
2026-06-06 18:33:47 +03:00
Дмитрий Соловьев a06dfcac4e docs: карта kafka/http/camunda взаимодействий сервисов 2026-06-06 00:12:27 +03:00
Дмитрий Соловьев 45fd6e8da9 docs: бэклог и бриф pcp-tgu-ops-ui, ui-прототипы tgu-ops
Добавлены docs/бэклог-pcp-tgu-ops-ui.md, бриф дашборда состояния группировки
и прототипы docs/ui-prototypes/tgu-ops/; удалён устаревший
docs/задачи для pcp-tgu-ops-ui.txt.
2026-06-06 00:04:36 +03:00
Дмитрий Соловьев 3239e7d26c pcp-tgu-ops-ui: правки API-клиентов, вкладок и стилей
complexPlanApi/missionPlanningApi, TguComplexPlanTab, TguEditorTab,
стили tgu-editor.css/tgu-planning.css.
2026-06-06 00:04:25 +03:00
Дмитрий Соловьев 1d07588690 backend: правки complex-mission/mission-planing/request + тестовые паспорта
ComplexPlanRunOrchestrationService, MissionController, MissionPlaningService,
KafkaProducerConfig; обновлены тестовые JSON-паспорта route-processing.
2026-06-06 00:04:17 +03:00
Дмитрий Соловьев b454c01c09 config: адреса config-server и окружения на localhost
application.yaml/yml всех сервисов и config-repo переведены с 192.168.60.x
на localhost (дефолты CONFIG_SERVER_URI и пр.); обновлены примеры в
changeIpInCurrentFolder.sh и create_pcp_databases.sh.
2026-06-06 00:04:09 +03:00
Дмитрий Соловьев 26bd1f111d build: Java 21 для всех модулей + чистка мёртвого versions.camunda
versions.java 17→21 (toolchain и Kotlin jvmTarget везде завязаны на это свойство;
рантайм-образы liberica-openjre 21 уже совпадают). pcp-tgu-service переведён с
хардкода of(21) на общее свойство. Удалён неиспользуемый versions.camunda=8.4.15
(оба потребителя Camunda уже на camunda-spring-boot-4-starter:8.8.10).
2026-06-05 23:53:30 +03:00
Дмитрий Соловьев 821de2b0c0 pcp-tgu-ops-ui: вкладка «Группировка» (состояние группировки) на реальных данных
Встраивание дизайн-хэндоффа constellation dashboard отдельной вкладкой рядом с
Планы/Карта/Комплексный план/Редактор/Заявки.

- модуль src/features/constellation/ (прототип на JSX + realData.ts/ConstellationTab.tsx)
- реальные данные вместо симуляции: платформы (OPERATIONAL), станции, заявки→ROI (WKT),
  трасса flight-line как эфемерида (сэмплер позиции через env.subSat), ЗРВ→сброс,
  съёмка по пролёту над ROI; оптика/локация — по имени КА из НСИ
- d3-geo + topojson-client; карта world-atlas локально (без CDN)
- изоляция стилей на .cdash (конфликт --line с theme.css), высота .app=100%
- локальное время в часах и оси таймлайна (конвенция проекта)
- allowJs:true в tsconfig.app.json под JSX-модуль
2026-06-05 23:37:47 +03:00
Дмитрий Соловьев ea207088bc pcp-tgu-ops-ui: правка включения через окно построения + обновление состава плана
Редактор: инспектор включения теперь только просмотр (время с десятыми долями
секунды), кнопки «Редактировать»→«Удалить». «Редактировать» открывает окно
построения, предзаполненное параметрами, и при сохранении заменяет маршрут.
Прицеливание опорной точки — явный режим: активно, пока не выбран проход.

Планы: состав включений выбранного плана перечитывается при возврате на вкладку
«Планы», иначе счётчик маршрутов оставался прежним после правок в редакторе.
2026-06-05 16:13:33 +03:00
Дмитрий Соловьев 995396da3b pcp-tgu-ops-ui: вкладка «Комплексный план» + слой карты
Новая вкладка планирования «Комплексный план» (TguComplexPlanTab) и
одноимённый слой на 2D-карте, выключенный по умолчанию.

- TguComplexPlanTab + complexPlanApi (через прокси /api/pcp-complex,
  добавлен в vite.config.ts) — данные из pcp-complex-mission-service;
- слой complexPlan: тип в mapLayerTypes/mapTypes/mapSelectionTypes,
  отрисовка drawComplexPlan, тоггл и подписи в Tgu2DMapTab/Tgu2DMapView;
- вкладка complex в TguActiveTab, кнопка в TguToolbar, рендер в
  TguPlanningPage; complexPlan: false в дефолтах слоёв и в редакторе/
  заявках/тесте интервала.
2026-06-05 14:51:59 +03:00
Дмитрий Соловьев 69dfd8025d pcp-tgu: слоты по факту закладки, запрет ранней закладки, шире рейл редактора
- слоты (acceptedPlanSlotsUpdate) занимаются на LAID_IN, а не на ACCEPT —
  до закладки план ещё может быть вытеснен коррекцией
- «Подтвердить закладку» недоступно, пока начало плана в будущем
- левая панель редактора 15.5rem → 17rem (цифры статистики у границы)
2026-06-04 21:15:13 +03:00
Дмитрий Соловьев bd2bc9d583 pcp-tgu: редактор — расчёт плана съёмки+сброса одной операцией, очистка плана, удаление включений
Вкладка «Редактор»:
- Кнопка «Рассчитать план» — запускает surveys/calculate → drops/calculate
  одной операцией на черновике (ensureDraft), затем перечитывает включения.
  Источник комплексного плана (snapshot этапа 4) выбирает бэкенд по интервалу.
  Вызовы идут напрямую в pcp-mission-planing-service (/api/pcp-mission).
  Ручные маршруты съёмки (source=MANUAL) сохраняются при пересчёте.
- Кнопка «Очистить план» — новый эндпоинт DELETE /api/missions/{id}/modes
  (clearPlan) сносит ВСЕ включения, включая ручные, с бронями и связями.
- Удаление маршрута съёмки / сброса в правой панели с подтверждением:
  WorkInspector.canDelete показывает кнопку удаления и для рассчитанных
  (read-only) включений, когда есть черновик; удаление через deleteMode.
2026-06-04 21:03:44 +03:00
Дмитрий Соловьев 9f1756a9f9 pcp-tgu-ops-ui: снятие плана с учёта + индикатор КА = нерешённый отказ
Индикатор «проблема КА» теперь означает НЕразрешённый отказ, а не «есть
хоть один REJECTED»: unresolvedProblemSpacecraftIds не красит КА, если
проблемный план снят с учёта (dismissedAt) либо цепочка уже продвинулась
за него (advanced-план со стартом не раньше). Исправленные и снятые
отказы гаснут сами.

- кнопка «Снять с учёта» / «Вернуть на учёт» в деталях плана для
  терминально-негативных статусов + бейдж «снят с учёта»;
- dismissPlan/restorePlan (через BFF), поле dismissedAt в модели;
- снятый план приглушён и штрихуется на таймлайне;
- тесты на unresolvedProblemSpacecraftIds.
2026-06-04 16:59:28 +03:00
Дмитрий Соловьев e6f1219c9c pcp-tgu: «снять с учёта» терминально-негативный план (dismissed_at)
Оператор деактивирует отменённый/неудавшийся план, чтобы он не считался
проблемой и не красил индикатор КА. Косметико-статистический флаг
dismissed_at: статус плана и состояние цепочки не трогает, обратим.

- planned_plan.dismissed_at (V14) + поле в сущности и PlanResponse;
- DismissPlanUseCase.dismiss/restore с гардом на терминально-негативные
  статусы (REJECTED, START_AMBIGUOUS, LAYIN_FAILED, NOT_ACCEPTED,
  SUPERSEDED), идемпотентно; эндпоинты POST /api/plans/{id}/dismiss|restore;
- BFF (pcp-ui-service): проброс dismissedAt в TguPlanResponse и
  /api/tgu-planning/plans/{id}/dismiss|restore;
- тест DismissPlanUseCaseTest; починен предсуществующий TguPlanningServiceTest
  (мок-JSON не отдавал projected — WebClient падал на null→boolean).
2026-06-04 16:59:13 +03:00
Дмитрий Соловьев fae57e957e pcp-tgu-ops-ui: «Создать план» берёт план в фокусе как базу-предысторию
Диалог получает initialBasePlanId — выбранный (в фокусе) план; селект
«Базовое состояние» предзаполняется им, если он годится в базу (тот же КА,
не PLANNED/DRAFT), иначе остаётся «без истории». Оператор может сменить
базу вручную.
2026-06-04 16:32:49 +03:00
Дмитрий Соловьев faadc63028 pcp-tgu-ops-ui: тест-кнопка закладки, статус SUPERSEDED, подсветка цепочки
- Тест-кнопка «Подтвердить закладку» в деталях плана для AWAITING_LAYIN
  (под подписью «ТЕСТОВЫЙ ФУНКЦИОНАЛ», с подтверждением). Гасится, если по
  КА уже заложен план со стартом в пределах ±10 мин (дублирует бэкенд-гард).
- Статус SUPERSEDED: стиль/подпись/легенда.
- Поиск уже созданного черновика — по слоту (КА + окно исходного плана),
  т.к. basePlanId черновика теперь = предшественник, а не исходный план.
- Таймлайн: при выборе плана непричастные планы гаснут, а текущий/прошлый/
  следующий обводятся яркими рамками (current #2563EB, prev #94A3B8,
  next #F59E0B). Асимметрия: prev (явная база выбранного) показывается
  всегда, даже REJECTED/SUPERSEDED; next (обратная ссылка) не подсвечивает
  мёртвые статусы — иначе вытесненная коррекция ложно числилась бы
  «следующей» у заложенного плана.
2026-06-04 16:13:13 +03:00
Дмитрий Соловьев 87905311b1 pcp-tgu: якорь цепочки на LAID_IN, параллельные выдачи, SUPERSEDED
Базой цепочки план становится на ЗАКЛАДКЕ (LAID_IN), а не на приёмке.
После принятого плана можно отправить коррекцию — она тоже примется;
несколько выдач «в полёте» по одному КА теперь норма, а не конфликт.

- ACCEPT больше не двигает currentBasePlanId/chainVersion и не запирает
  слот — только переводит план в AWAITING_LAYIN.
- Новый позитивный топик layin-confirmed + LayinConfirmedConsumer →
  handleLayinConfirmed: AWAITING_LAYIN → LAID_IN, план становится базой
  (chainVersion++), конкуренты на пересекающемся окне гасятся в новый
  терминальный статус SUPERSEDED. Гард ±10 мин (LAYIN_MIN_SEPARATION_MINUTES):
  нельзя заложить план со стартом в пределах 10 мин от уже заложенного (409).
- «В полёте» считается из статусов планов (ISSUING/WAITING_DECISION), а не
  из единственного active-слота; снят гард «идёт выдача другого плана».
  IssueDuePlanWorker гейтится hasBlockingInFlight, базу нового плана берёт
  как latestCommittedPlanId; grace больше не двигает базу.
- PlanProjectionService якорит по самому дальнему committed-окну
  {ISSUING, WAITING_DECISION, AWAITING_LAYIN, LAID_IN}.
- REJECT ставит паузу только если по КА не осталось живых кандидатов.
- CreateDraftUseCase: replace-черновик наследует ПРЕДШЕСТВЕННИКА исходного
  плана (base.basePlanId), а не сам план — коррекция занимает тот же слот;
  fork-forward по-прежнему ссылается на базу. Идемпотентность черновиков —
  по слоту (КА + окно), а не по basePlanId.
- Тест-контроллер: POST /test/tgu/plans/{id}/confirm-layin (через BFF
  pcp-ui-service) — имитация события «план заложен».
- Док PCP_TGU_PLAN_STATUS_LIFECYCLE обновлён (LAID_IN-переход, SUPERSEDED).
2026-06-04 16:13:02 +03:00
Дмитрий Соловьев 5235142fa6 pcp-tgu: редактор — таймлайн зум/драг/секундная шкала, подтверждения в деталях плана, без авто-кнопок
- детали плана: подтверждение (confirm) на «Отправить» и «Удалить» черновика
- редактор: убраны кнопки «Применить»/«Отмена», undo/redo и сдвиг ◀▶ из тулбара
- окно: предел отдаления план±1ч, дефолт тоже план±1ч; зум кнопками и колесом мыши (центр — курсор)
- таймлайн: drag по горизонтали как карту; минимум окна ~10с
- ось времени: адаптивные деления от секунд до недель, секунды в подписи при мелком масштабе
2026-06-04 14:20:01 +03:00
Дмитрий Соловьев 9e7cae4a56 docs: актуализация списка задач pcp-tgu-ops-ui (закрытые пункты, витки/полосы обзора) 2026-06-04 13:30:21 +03:00
Дмитрий Соловьев f0e56b6e2a pcp-tgu: редактор — стабильные витки, плейсхолдер без плана, черновик без авто-перехода
Вкладка «Планы»:
- «Создать черновик на основе плана» больше не перекидывает в редактор: черновик
  создаётся, выбирается на схеме (тумблер «Черновики» вкл.) и показывается подсказка —
  оператор сам переходит в «Редактор».

Редактор:
- Витки больше не пропадают «иногда»: жизненный цикл flightLineData теперь полностью
  внутри fetch-эффекта на [noradId, mapRange] (сброс + перезагрузка атомарны). Раньше
  трассу сбрасывал общий reset по ссылке selectedPlan, а перезагружал отдельный эффект —
  фоновый loadData пересобирал selectedPlan с тем же окном, mapRange по значению не менялся,
  фетч не перезапускался, витки молча исчезали.
- Явный индикатор вместо немой пустоты: «Загрузка трассы…» / «Нет данных трассы на это окно».
- После удаления открытого черновика редактор не «всплывал» обратно: guard загрузки включений
  не тянет режимы по устаревшему draftPlanId, когда план не выбран.
- Редактор включений гейтится на наличие плана (план == миссия): без плана — плейсхолдер
  «Выберите КА и план…». Иначе после удаления черновика вкладка оставалась активной по КА
  (витки, режим съёмки), хотя редактировать нечего.
2026-06-04 13:30:12 +03:00
emelianov b62a7cf8ef ПУУД 2026-06-04 13:22:48 +03:00
Дмитрий Соловьев 9e5460d646 pcp-tgu: редактор/карта — выбор объекта (линии/сбросы) и проброс текста ошибок
- Карта 2D: клик по линии (трасса КА / сеанс сброса) выбирает объект — общий
  инспектор с таймлайном; MapLine.name, тип selection "line" (kind track/drop),
  связь с работой через modeId; spatialIndex учитывает линии.
- Редактор (TguEditorTab/EditorPanels): единый WorkInspector для клика по карте
  и по таймлайну, подсветка соответствующего включения, стили tgu-editor.css.
- config-repo: server.error.include-message=always для pcp-tgu-service и
  pcp-mission-planing-service — UI показывает причину 4xx (reason/exception),
  а не только HTTP-фразу.
2026-06-04 13:08:08 +03:00
Дмитрий Соловьев d9f8260120 pcp-tgu: вкладка «Планы» — черновик на основе плана + правки UX
- Кнопка «Создать черновик на основе плана» в деталях плана: создаёт
  replace-черновик (окно 1:1, все включения клонируются) и открывает редактор.
  Путь «сделать исправленный план» вместо повторной отправки. Доступна для
  персистентных правимых баз; DRAFT/PLANNED/LAID_IN исключены (для LAID_IN
  replace 1:1 запрещён). Если черновик по базе уже есть — открываем его.
- Диалог создания плана цепочки: вместо списка будущих планов и «Отправить
  вручную» — кнопка «Редактировать» (сразу в редактор черновика).
- Сайдбар: повторный клик по выбранному КА снимает выбор (показать все КА).
2026-06-04 13:07:09 +03:00
Дмитрий Соловьев fd3603a086 pcp-tgu: статусы плана — актуальная легенда с тултипами + док жизненного цикла
- STATUS_STYLES в порядке жизненного цикла: убран легаси ACCEPTED, добавлены
  LAID_IN/NOT_ACCEPTED/LAYIN_FAILED, у каждого статуса человеческое описание.
- Легенда вкладки «Планы»: тултипы по наведению (title=description), убрана
  устаревшая приписка про стрелки.
- TimelineStatusStyle: добавлено обязательное поле description.
- HandlePlanDecisionUseCase: TODO(layin-confirmed) — обработчик приёма события
  «план заложен» (AWAITING_LAYIN → LAID_IN) ещё не реализован.
- docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md: справочник по каждому статусу
  (когда наступает / что значит / какой переход), диаграмма, зарезервированный ACCEPTED.
2026-06-04 13:06:58 +03:00
Дмитрий Соловьев a5a1cdfa64 pcp-tgu: связь планов на таймлайне по basePlanId вместо стрелок/нумерации по времени
Стрелки и бейджи-цифры цепочки строились по ВРЕМЕНИ (соседние по времени
планы КА) и ложно сцепляли два черновика с одной предысторией как «один —
предок другого». Убираем эту логику целиком (buildSequentialLinks,
buildPlanChains, CHAIN_COLORS/chainColor, тип TimelineLink) и показываем
связь только по реальному basePlanId:

- В деталях плана — строка basePlanId (рендерится, если задан).
- При выборе плана на таймлайне подсвечиваем гало два соседних звена по
  basePlanId: предыдущий (planId == basePlanId выбранного) — тёплый янтарь,
  следующий (basePlanId == выбранный) — бирюза. Следующих может быть
  несколько (черновики с одной предысторией) — подсвечиваются все.

Бэкенд: basePlanId/projected проброшены через PlanResponse (pcp-tgu-service)
и TguPlanResponse (pcp-ui-service — прокси /api/tgu-planning/plans, иначе
Jackson срезал бы неизвестные поля). Требует рестарт обоих сервисов.

Заодно: очистка нотиса деталей при смене фокуса (план/ЗРВ) и блок
«Включения» (счётчики съёмок/сбросов) в правой панели.
2026-06-04 11:44:28 +03:00
Дмитрий Соловьев 294b17757d pcp: время — сквозной naive LocalDateTime как локальное настенное время (без UTC)
Отказ от остаточной UTC-семантики: naive LocalDateTime трактуется как
локальное время единой зоны всех сервисов.

Backend (границы внешних систем):
- VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase: ZoneOffset.UTC →
  ZoneId.systemDefault() для проекции внешних Instant/ISO-с-Z в naive.
- тесты границ перевёрнуты на systemDefault (под Europe/Moscow ждут +3).
- чистка UTC-формулировок в DTO-комментариях, мёртвых ссылок на
  docs/standards/DATETIME_UTC.md.

Frontend (pcp-tgu-ops-ui):
- utils/utcTime.ts → utils/localTime.ts: parseLocal/formatLocal*/toLocalIso/
  toLocalInputValue с локальной семантикой (без дописывания Z и timeZone:UTC).
- замена getUTC*/Date.UTC/toISOString-сериализации на локальные аналоги.
- убраны пользовательские подписи «UTC» в редакторе/таймлайне.
- удалён utcTime.guard.test.ts (форсил обратный UTC-инвариант); тест-фикстуры
  сделаны зоно-независимыми. tsc + vitest зелёные под TZ=UTC и TZ=Europe/Moscow.

Включает также прочие незакоммиченные правки рабочего дерева.
2026-06-04 09:50:39 +03:00
emelianov faedd21f43 расчет ПУУД 2026-06-04 09:39:27 +03:00
Дмитрий Соловьев 5049395911 pcp-tgu: пауза цепочки после REJECTED + режим «Черновики» + UI цепочек/ЗРВ
Бэкенд (pcp-tgu-service):
- REJECTED трактуется как «план неправильный»: авто-перевыдача больше НЕ
  взводится, цепочка встаёт на паузу (новый флаг
  SpacecraftPlanningStateEntity.pausedAfterReject, миграция V13). Технические
  негативы («не заложен»/«не принят») по-прежнему дают срочную перевыдачу.
- Пауза централизована в PlanProjectionService.project(): для отклонённой
  цепочки проекция пустая, воркер выдачи ничего не материализует. Флаг
  снимается при ручной выдаче/промоуте черновика/приёмке (оператор вмешался).
- Режим «Черновики»: GET /plans/drafts (опц. ?spacecraftId) -> только DRAFT.
- Гард дублей черновиков: (КА + начало + конец) уникальны среди DRAFT,
  повторное создание окна -> 409.
- ZrvWindowMatcher: сравнение по истинной abs-длительности Duration (<=1с),
  устойчиво к усечению ISO на фронте до целых секунд.

Фронт (pcp-tgu-ops-ui): вкладка «Планы» — таймлайн цепочек, детали плана,
панель ЗРВ (TguZrvPanel), диалог создания цепочки, режим черновиков.

Удалён устаревший docs/tasks/TASK_tgu-plan-versioning-manual-reissue.md.
2026-06-04 00:04:47 +03:00
Дмитрий Соловьев 3638afdb85 pcp: откат UTC-слоя — сквозной naive LocalDateTime без зоны на проводе
Возврат к модели «настенных часов»: одна ISO-строка без зоны
("2026-05-29T01:00:00") от БД до экрана, никто не привязывает её к
часовому поясу, сдвигаться нечему.

Бэкенд — снять зону с границы API:
- DTO OffsetDateTime/Instant -> LocalDateTime и убрана конвертация
  atOffset(ZoneOffset.UTC)/toUtcOffset() в фабриках и мапперах:
  tgu PlanResponse/NewChainDTOs/TguPlanningDTO, request RequestApiDtos,
  complex ComplexPlanRun*DTO, dynamic ObservationParametersDebugDTO.
- Удалён толерантный Jackson-слой: PcpTimeModule(+autoconfig, ServiceLoader,
  imports), PcpTimeModuleV3, LocalDateTimeTimeZoneSerializer; снята их
  регистрация в Camunda/Rest/dynamic JacksonConfig. Штатный JSR-310
  (JavaTimeModule + WRITE_DATES_AS_TIMESTAMPS=false) оставлен.
- Kafka-конверт KafkaMessage.time сериализуется ISO без офсета.
- mission-planing Mode/SurveyMode/DropMode entity и репозитории/сервис
  переведены на LocalDateTime; колонки БД остаются timestamptz (Hibernate
  мапит на naive без миграции — прецедент MissionEntity/PlanEntity).
- RequestServiceClient (dynamic) синхронизирован по типу с request-service.

Анти-регрессия: LocalDateTime.now(ZoneOffset.UTC) для серверных штампов
оставлен (выбор настенных часов UTC, не зона на проводе).

Фронт — снять офсет с egress: toUtcIso/фильтры отдают naive без Z
(после удаления толерантного десериализатора штатный парсер падал бы на Z).
Дисплейные хелперы (показ «как пришло», без сдвига) сохранены.

Тесты: контрактные ждут naive ISO; удалены UTC-артефакты (нормализация
офсетов) и тесты прослоек. Удалён docs/standards/DATETIME_UTC.md.

Операционно перед выкатом: сервисы катить согласованно; Kafka-топики с
ранее опубликованным офсетом +00:00 дренировать/проиграть.
2026-06-04 00:03:52 +03:00
Дмитрий Соловьев 0e534c5bd5 docs: удалить неактуальные TASK-доки (datetime-utc, tgu-editor-integration, plan-on-demand-redesign) 2026-06-03 16:26:03 +03:00
Дмитрий Соловьев 9c2d8516bc pcp: создание плана новой цепочки из вкладки «Планы» (база/без истории, окно из двух ЗРВ)
Оператор берёт за основу прошлый план (предыстория) или начинает цепочку с нуля,
набирает окно из двух реальных ЗРВ и создаёт план-кандидат; сразу показывается прогноз
будущей цепочки от него. Рабочим (якорем) план становится после ручной отправки (promote)
и приёмки — отдельного механизма якорения не вводим.

pcp-tgu-service:
- сырые ЗРВ: PlanCalculationService.rawWindows + PlanProjectionService.rawZrvWindows,
  GET /api/plans/{spacecraftId}/zrv (исходные окна снапшота, не cooked-проекция);
- CreateChainPlanUseCase + POST /api/plans/new-chain: опциональная база (клон включений
  в окно) либо пустая миссия без истории; прогноз цепочки через projectAfterWindow;
- ZrvWindowMatcher (старт+КПП одной зоны, конец — другой); assertDraftSlotStillValid
  переведён на него — единая проверка для replace/fork-forward и окна из двух ЗРВ;
- тесты: CreateChainPlanUseCaseTest (8), ZrvWindowMatcherTest (7), правки promote-тестов.

pcp-mission-planing-service:
- POST /api/missions/blank — пустая миссия для сценария «без истории» (+ 2 теста).

pcp-tgu-ops-ui:
- API fetchZrvWindows/createChainPlan; диалог «Создать план» во вкладке «Планы»
  (выбор базы, стартовой и конечной ЗРВ) с показом спрогнозированной цепочки и ручной
  отправкой; подсветка ЗРВ выбранного КА на таймлайне;
- «Целевая ЗРВ» убрана из редактора (он остаётся под replace-правку); удалён ставший
  мёртвым editorForkForward; createChainWindows + тесты.
2026-06-03 16:25:46 +03:00
Дмитрий Соловьев 057452f19d pcp-tgu-ops-ui: fork-forward в редакторе — выбор будущей ЗРВ для перевыдачи невозвратного плана
Селектор «Целевая ЗРВ» в EditorToolbar: текущий слот (replace, по умолчанию)
либо будущая ЗРВ из проекции pcp-tgu-service (fetchSpacecraftPlans →
GET /api/plans/{spacecraftId}, фильтр futureZrvWindows: PLANNED строго в
будущем, кроме базы — та же проекция, что валидирует бэк). Выбор окна
переводит обзор редактора и заставляет ensureDraft слать mode=fork-forward
(naive-UTC окно + kpp); селектор блокируется после создания DRAFT.
2026-06-03 14:12:21 +03:00
Дмитрий Соловьев 2a841fc7b3 tgu: DELETE /plans/{draftPlanId} — отказ от черновика (бэкенд + кнопка «Отмена» и авто-отмена в редакторе) 2026-06-03 13:59:52 +03:00
Дмитрий Соловьев ec67d9d83d pcp-tgu-ops-ui: Ф6 ядро — draft/validate/promote в редакторе (replace), прямой прокси /api/pcp-tgu 2026-06-03 13:53:24 +03:00
Дмитрий Соловьев ae7effe1fa pcp-tgu-service: Ф5 — promote черновика с оптимистичным локом базы (POST /plans/{id}/promote) 2026-06-03 13:36:59 +03:00
Дмитрий Соловьев 8133a5144f pcp-mission-planing: Ф4 — валидация-заглушка POST /missions/{id}/validate (контракт конфликтов, пустой diff) 2026-06-03 13:27:31 +03:00
Дмитрий Соловьев f5211fa5a0 pcp-mission-planing: Ф3 — правки контента в черновик (PATCH съёмки, DELETE включения, без авто-создания живой миссии) 2026-06-03 13:20:54 +03:00
Дмитрий Соловьев 85ce238f52 pcp: Ф2 — доработки по ревью (валидация ошибок клона, идемпотентность DRAFT, тесты)
- MissionPlaningClient: маппинг ошибок mission-planing (4xx как есть, 5xx → 502) + таймаут 30с
- CreateDraftUseCase: гард идемпотентности — второй открытый DRAFT на тот же план → 409
- cloneMission: явная валидация mode (неизвестный → 400 вместо тихого полного клона)
- константы DraftMode/CloneMode вместо строковых литералов
- kdoc: семантика fork-forward (границы-слот vs времена включений; съёмка без сброса)
- тесты: CreateDraftUseCaseTest (13 кейсов), MissionCloneTest (+съёмка без сброса)
2026-06-03 13:09:52 +03:00
Дмитрий Соловьев 0d69617420 pcp: Ф2 — черновик-клон миссии (replace + fork-forward)
mission-planing: POST /api/missions/{id}/clone — полный клон (replace)
или по перекрытию с окном (fork-forward, [start,stop) открытый правый конец);
пустые drop-mode (все съёмки вне окна) не клонируются

tgu-service: PlannedPlanStatus.DRAFT; POST /api/plans/{planId}/draft
(CreateDraftUseCase: guard replace/fork-forward, clone mission → planId=newMissionId,
компенсация при сбое); DRAFT отфильтрован из PlanQueryService read-пути;
MissionPlaningClient + MissionPlaningProperties + mission-planing.base-url в yaml

тесты: MissionCloneTest (6 граничных кейсов предиката перекрытия),
PlanQueryServiceTest (DRAFT не видна в ответе)
2026-06-03 12:53:57 +03:00
Дмитрий Соловьев 80e281dd6d pcp-tgu-ops-ui: панель «Параметры включения» в инспекторе маршрута
- WorkInspector показывает аппарат, виток, крен, угол захвата, центр контура (съёмка)
  и станцию/сбрасываемые съёмки (сброс)
- planModesToWorks/addShoot прокидывают planModeId, revolution, rollDeg, captureAngleDeg,
  centerLat/Lon, surveyIds в модель включения
- место под перечень заданий маршрута (привязка — позже)
- секция «Станция приёма» рендерится только в режиме правки
2026-06-03 12:40:53 +03:00
Дмитрий Соловьев e060e13b85 pcp-tgu-service: боевой POST /api/plans/{planId}/issue + гард точки невозврата
- ручная выдача вынесена из TestTguController в PlanController
- assertReissuableInSlot: 422 при LAID_IN и заморозке (now >= start - layinFreezeMinutes),
  409 при активной выдаче другого плана КА; перевыдача того же плана поверх своей
  in-flight attempt разрешена (новая attemptNo++ замещает летящую)
- REISSUABLE_STATUSES удалён — гард по времени/статусу его заместил
- layinFreezeMinutes (деф. 10) в PlanningProperties + config-repo/application yaml
- граничные тесты в IssueDuePlanWorkerOnDemandTest, переписан PlanIssueAndDecisionFlowTest
- дизайн-док версионирования (фаза 1 отмечена сделанной)
2026-06-03 12:40:03 +03:00
Дмитрий Соловьев c32e13e09c pcp-tgu-ops-ui: гасить кнопки решения по истечении grace-окна WAITING_DECISION 2026-06-03 10:00:44 +03:00
Дмитрий Соловьев 9a2ba655e1 pcp-ui-service: удалить мёртвый реактивный CorsFilterComponent (сервлет-стек его не вызывает) 2026-06-03 09:45:24 +03:00
Дмитрий Соловьев 938248d150 pcp-tgu-ops-ui: крен маршрута — один знак после запятой 2026-06-03 09:43:10 +03:00
Дмитрий Соловьев 17b8679e44 pcp-ui-service: CORS allowedOriginPatterns — фикс Invalid CORS request на POST решения плана 2026-06-03 09:43:10 +03:00
Дмитрий Соловьев b61be0afb8 pcp-tgu-ops-ui: маска ввода времени — невозможные значения недопустимы
Time24Field принимал любой текст (99:99 и т.п.), валидным он был только при
коммите/блюре. Добавлена маска maskTime: на каждый ввод оставляет цифры,
расставляет двоеточия и зажимает сегменты (часы 0-23, мин/сек 0-59) с авто-
дополнением первого разряда, поэтому набрать невозможное время нельзя.
Покрыто юнит-тестами (timeFields.test.ts).
2026-06-03 09:12:16 +03:00
Дмитрий Соловьев 51fd8a446b pcp-tgu-ops-ui: надёжный 24ч ввод времени (своё текстовое поле вместо нативного)
Нативные input type=time/datetime-local рендерят время по локали браузера/ОС
(AM/PM) и игнорируют атрибут lang в Chrome — поэтому lang=ru-RU не помогал.
Заменено на собственные контролы (components/timeFields.tsx):
- Time24Field — текстовый ввод ЧЧ:ММ[:СС] с валидацией, детерминированный 24ч;
- DateTime24Field — нативный календарь для даты + Time24Field для времени.
Подключено в окне маршрута редактора и во всех datetime-local фильтрах
(Планы, заявки на карте, создание/фильтр заявок).
2026-06-03 09:02:27 +03:00
Дмитрий Соловьев f0e16aa4e8 pcp-tgu-ops-ui: одно перекрестие опорной точки + 24ч формат времени
- Редактор: убрано накопление целей съёмки — на карте только одно
  перекрестие текущей точки (pickTarget заменяется при каждом клике),
  с перекрестия убрана подпись. Правая панель переименована в
  «Опорная точка прицеливания» с координатами.
- 24-часовой формат: lang="ru-RU" на всех datetime-local инпутах
  (Планы, фильтры заявок на карте, создание/фильтр заявок), иначе
  браузер рендерил их по локали ОС в AM/PM.
2026-06-02 23:33:41 +03:00
Дмитрий Соловьев e68c17e792 pcp-tgu-service: миграция с WebFlux на servlet/MVC
WebFlux в tgu был фиктивным: блокирующая JPA, контроллеры оборачивали
блокирующие вызовы в Mono.fromCallable{}.subscribeOn(boundedElastic()),
WebClient .block()-ался сразу. Единственный WebFlux-only сервис флота.

- build: подключён pcp-types-lib (тянет starter-web → servlet), springdoc
  webflux-ui → webmvc-ui; starter-webflux оставлен только ради WebClient
- контроллеры возвращают значения напрямую (без Mono-обёрток)
- удалено локальное Jackson-2 зеркало PcpTimeModule.kt → модуль из либы
  в CamundaJacksonConfig; Jackson-3 зеркало остаётся (Boot 4 держит
  Jackson 3 на HTTP-границе и для MVC), WebFluxJacksonConfig → RestJacksonConfig
2026-06-02 23:14:12 +03:00
Дмитрий Соловьев cde4ffa9b6 pcp-tgu-service: толерантность времени на Jackson-3 пути (REST-кодеки WebFlux) 2026-06-02 22:57:49 +03:00
Дмитрий Соловьев 84e70350b9 pcp-types-lib: Kafka-конверт time в UTC (фикс сдвига пояса)
KafkaMessage.time: LocalDateTime.now() -> now(ZoneOffset.UTC);
LocalDateTimeTimeZoneSerializer: atZone(systemDefault()) -> atOffset(UTC).

Было: локальное now() + systemDefault давали на проводе "...+03:00", и
потребитель (паттерн ...SSS[xxx] в LocalDateTime) дропал зону -> сдвиг на пояс.
Стало: "...+00:00" — инстант тот же, но стенные часы совпадают с UTC, сдвиг ушёл.

Тест LocalDateTimeTimeZoneSerializerTest: UTC-офсет без сдвига + round-trip.
DATETIME_UTC.md: баг перенесён в «Исправленные».
2026-06-02 22:48:13 +03:00
Дмитрий Соловьев 405f02aa86 pcp: слой толерантности времени — читать naive(=UTC) и Z во всём флоте
PcpTimeModule (libs/pcp-types-lib): толерантные Jackson-десериализаторы
LocalDateTime/OffsetDateTime/Instant — любой потребитель читает оба формата
на проводе и нормализует к UTC. Только чтение, формат записи не меняется.

Подключение фронтом по флоту:
- Spring Boot auto-configuration + AutoConfiguration.imports — Boot-маппер
  всех 12 сервисов на pcp-types-lib (без правок в сервисах);
- Jackson SPI (META-INF/services) — ручные мапперы с findAndRegisterModules();
- точечно: dynamic-plan/JacksonConfig (ручной ObjectMapper);
- tgu (WebFlux, не зависит от либы) — локальное зеркало модуля в
  CamundaJacksonConfig (Jackson-2 путь).

Зачем: микросервисы деплоятся независимо; с толерантностью продюсера можно
флипать LocalDateTime -> OffsetDateTime (Z) в любом порядке без синхронного
деплоя — потребитель не падает на парсинге офсета.

Тесты: PcpTimeModuleTest (оба формата -> один UTC-момент + авто-подхват SPI).
Доки: DATETIME_UTC.md (раздел 2bis + найденные баги формата), CHECKLIST.md.
2026-06-02 22:35:50 +03:00
Дмитрий Соловьев 240e12f442 pcp-tgu-ops-ui: вкладка Редактор — шаг времени маршрута и формат длительности
- RouteTimeStepper и доработки EditorPanels/TguEditorTab по вводу времени;
- formatDuration: секунды/минуты для коротких интервалов;
- стили tgu-editor.css.
2026-06-02 22:06:06 +03:00
Дмитрий Соловьев c2e2b2922f pcp: миграция времени плана ТГУ на явный Z (OffsetDateTime)
PlanResponse (pcp-tgu-service) и зависящий от него TguPlanResponse (pcp-ui-service)
переведены с naive LocalDateTime на OffsetDateTime в UTC → на проводе явная зона Z.
Домен/БД (PlannedPlanEntity/CalculatedPlan) остаются naive LocalDateTime (= UTC);
конвертация только на границе API (atOffset(ZoneOffset.UTC)). Потребитель один
(ui-service.TguPlanningService), обновлён синхронно; фронт parseUtc совместим с Z.

- контрактные тесты обновлены на Z (PlanResponseContractTest, TguPlanningDtoContractTest);
- тестовые фикстуры/ассерты приведены к OffsetDateTime/Z (PlanQueryServiceTest,
  CatalogControllerTest, TguPlanningServiceTest);
- docs/standards/DATETIME_UTC.md: отражена миграция и обновлена таблица сервисов.
2026-06-02 22:02:13 +03:00
Дмитрий Соловьев 260583b134 pcp: единообразие времени (UTC) по группе сервисов
Фронт (pcp-tgu-ops-ui):
- тест-страж utcTime.guard.test.ts: запрещает Date.parse / new Date(string) /
  toLocale* без timeZone вне utils/utcTime; тестовые фикстуры переведены на parseUtc.

Бэкенд:
- UTC-now: LocalDateTime.now() -> LocalDateTime.now(ZoneOffset.UTC) по всем сервисам
  (entity-дефолты, воркеры, use case'ы) и в тестовых фикстурах.
- Конвертации к UTC (баги на не-UTC JVM, каждая с регресс-тестом в не-UTC поясе):
  - tgu: VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase (Instant -> UTC LocalDateTime);
  - mission-planing: MissionPlaningService (OffsetDateTime -> UTC через toUtcLocalDateTime);
  - dynamic-plan: RequestServiceClient (OffsetDateTime -> UTC);
  - slots: SlotRepositoryImpl (getObject(LocalDateTime) вместо getTimestamp().toLocalDateTime()).
- Контрактные сериализационные тесты формата на проводе (naive без зоны / Z):
  PlanResponse (tgu), RequestResponseDto (request, эталон), TguPlanning DTO (ui).
- KDoc-инвариант «=UTC» на UI-facing DTO (tgu PlanResponse, ui-service DTO вкладки ТГУ).

Документация: docs/standards/DATETIME_UTC.md (соглашение, таблица сервисов, под-задачи на миграцию к Z),
docs/standards/CHECKLIST.md (пункт про UTC на API).
2026-06-02 21:51:38 +03:00
Дмитрий Соловьев ac2220d063 pcp-tgu-ops-ui: вкладка Редактор — план, включения, нижняя панель
- Слева выводим даты начала/конца плана и корректные счётчики
  съёмки/сброса (из plan modes, а не из заглушки-черновика).
- На таймлайне показываем реальные включения плана (read-only): убран
  фейковый «сброс на весь интервал»; вместо линии «сейчас» — маркеры
  начала и конца плана; клик по включению открывает его инфо справа.
- Включения съёмки и сброса разнесены на два ряда (верх/низ) и различаются
  цветом, слева — подписи рядов.
- Нижняя панель: активная дорожка резиновая — примыкает к низу и не
  пустует, при подъёме панели ряды расширяются; высота бара ограничена
  потолком и центрируется в ряду, чтобы узкие включения не вытягивались.
- Время в редакторе парсится/форматируется через utils/utcTime.
2026-06-02 16:54:01 +03:00
Дмитрий Соловьев fb0332a030 pcp-tgu-ops-ui: единый UTC во времени; дефолтный диапазон Планы
Время бэка (naive LocalDateTime и OffsetDateTime) разбиралось через
new Date(string)/Date.parse, из-за чего naive-строки трактовались как
локальное время браузера: вкладки расходились по времени, а путь создания
заявки сдвигал введённое время на офсет. Ввёл единую точку utils/utcTime
(parseUtc/formatUtc*/toUtcInputValue/toUtcIso) и перевёл на неё вкладки
«Планы», «Заявки», «Карта 2D» — чтение, пикеры диапазона, создание заявки.

Плюс на вкладке «Планы» график по умолчанию открывается на интервал
сейчас−30 мин … сейчас+3 дня.

docs/tasks/TASK_datetime-utc-consistency.md — ТЗ на сверку всей группы
сервисов pcp на единообразие времени.
2026-06-02 16:53:48 +03:00
Дмитрий Соловьев faf264e416 pcp-tgu-ops-ui: маршруты съёмки на карте — фикс антимеридиана
drawPlanWorks проецировал точки напрямую и проверял видимость по одной
копии мира: полигон, пересекающий 180°, рисовался через всю карту, а слой
мигал «то есть, то нет» при зуме/панораме. Перенёс паттерн из
drawRequests/drawSwath: unwrapX (склейка контура через шов) + отрисовка в
трёх копиях мира с culling по каждой.
2026-06-02 16:53:32 +03:00
Дмитрий Соловьев a61e23c095 pcp-tgu-service: не дублировать выданный план проекцией
Проекция будущей цепочки якорилась только на принятый базовый план
(currentBasePlanId) и игнорировала выданный, но ещё не принятый план в
полёте (ISSUING/WAITING_DECISION). Из-за этого она заново выводила уже
выданный слот, и при дрейфе снапшота ЗРВ (сместился КПП или границы окна)
точечный дедуп по businessKey в PlanQueryService не срабатывал — на
вкладке «Планы» появлялся призрак-дубль PLANNED поверх WAITING_DECISION.

Теперь якорь проекции — самая дальняя зафиксированная точка цепочки: если
есть план в полёте, цепочку строим после него. На путь выдачи не влияет:
там activePlanId к моменту вызова проекции уже снят.
2026-06-02 16:53:24 +03:00
Дмитрий Соловьев 061ac1390c pcp-tgu-ops-ui: показывать только КА со статусом OPERATIONAL
С остальными КА мы не работаем, поэтому отсекаем их сразу на входе в
loadData: фильтруем платформы по статусу и оставляем планы только для
эксплуатируемых КА (по norad-id), иначе неoperational-аппарат вернулся бы
fallback-строкой по плану в buildPlatformsForInterval.
2026-06-02 15:00:30 +03:00
Дмитрий Соловьев e738c236a3 Не блокировать планирование неподтверждённым планом
Если план простоял в WAITING_DECISION дольше waiting-decision-grace-minutes
(по умолчанию 30) после своего начала и решения по нему так и нет — перестаём
ждать подтверждение: снимаем активную выдачу, двигаем якорь цепочки на этот
план и выдаём следующий. Статус плана не меняется (остаётся WAITING_DECISION,
не ACCEPTED) — это молчаливое продолжение цепочки, а не приёмка. Запоздалое
решение по такому плану просто пишется на attempt и не откатывает ушедшую
вперёд цепочку.
2026-06-02 14:51:43 +03:00
Дмитрий Соловьев 86215b300a pcp-tgu-ops-ui: убрать SUPERSEDED/EXPIRED и чекбокс истории на вкладке «Планы»
Статусы SUPERSEDED/EXPIRED удалены из модели tgu-service, поэтому чистим их и
во фронте, чтобы не описывать несуществующие состояния.

- tguTypes: убраны EXPIRED/SUPERSEDED из union TguPlanStatus.
- tguStatus: удалены их стили; заодно снят осиротевший фильтр
  isActivePlanStatus/ACTIVE_PLAN_STATUSES (после удаления чекбокса не нужен).
- tguTimelineMapper: EXPIRED убран из PROBLEM_STATUSES.
- tgu-planning.css: удалены классы .status-superseded/.status-expired и
  осиротевший блок .tgu-timeline-controls.
- TguPlanningPage: убран тумблер «Показать историю версий» вместе со стейтом
  showHistory и фильтром timelinePlans — таймлайн рендерит все планы из API
  напрямую (перестали скрываться AWAITING_LAYIN/REJECTED/LAYIN_FAILED/NOT_ACCEPTED).

EXPIRED в requestTypes (статус заявок) — отдельный домен, не тронут.
2026-06-02 14:33:04 +03:00
Дмитрий Соловьев c76a823dbc Редизайн pcp-tgu-service: хранить только выданное, будущее считать на лету
Раньше вся будущая цепочка планов материализовалась в planned_plan как
PLANNED-строки, а отдельный воркер пересчёта (RebuildPlanScheduleUseCase +
PlanRecalculationJob) держал её в актуальном состоянии и гасил выпавшие планы
в SUPERSEDED/EXPIRED. Это плодило вложенные планы, гонки пересчёта и мусор в БД.

Новая модель: в БД живёт только выданное (история навсегда), будущее — проекция,
вычисляемая по запросу из ЗРВ. Перекрывать (supersede) и протухать (expire)
больше нечего.

Бэкенд:
- Удалены TrackedPlan (entity/source/status + repo) и PlanRecalculationJob
  (entity/repo/service/worker) — материализация и воркер пересчёта больше не нужны.
- Удалён RebuildPlanScheduleUseCase; будущее строит новый PlanProjectionService
  поверх PlanCalculationService.
- PlanQueryService отдаёт проекцию (выданное из БД + будущее на лету).
- Новый флоу реакции на смену параметров станции: StationParamsChangedConsumer +
  HandleStationParamsChangedUseCase (срочный перевыпуск через planning_state).
- Новые консьюмеры обратной связи исполнения: LayinFailedConsumer,
  PlanNotAcceptedConsumer (+ DTO PlanExecutionFeedbackMessage,
  StationParamsChangedMessage).
- PlannedPlanStatus: убраны мёртвые SUPERSEDED/EXPIRED; статусы выровнены под
  жизненный цикл выдачи/закладки.

Миграции (ddl-auto: validate):
- V7  drop tracked_plan
- V8  drop plan_recalculation_job
- V9  planning_state: поля срочного перевыпуска
- V10 снять unique-ограничение бизнес-ключа planned_plan
- V11 удалить легаси PLANNED-строки (материализованное будущее)
- V12 удалить легаси SUPERSEDED/EXPIRED и столбец superseded_by_plan_id

Тесты: удалены JobRerunFlowTest и RebuildPlanScheduleUseCaseTest; добавлены
PlanProjectionServiceTest, PlanQueryServiceTest, IssueDuePlanWorkerOnDemandTest,
HandleStationParamsChangedUseCaseTest, VisibilityPayloadParserTest. Скорректированы
PlanIssueAndDecisionFlowTest, PlanCalculationServiceTest, VisibilityFlowTest и др.

V11/V12 необратимы — перед миграцией на проде снять дамп planned_plan.
2026-06-02 14:32:51 +03:00
Дмитрий Соловьев d630a891f6 Тест: провал видимости перекрывается одним планом (форма Reflexio)
Воспроизводит форму с экрана Reflexio (раннее окно, большой провал
видимости, далёкий кластер) и проверяет, что цепочка перекрывает провал
одним длинным планом и доходит до далёкого кластера без разрыва.
2026-06-02 11:32:16 +03:00
Дмитрий Соловьев e0aa1ccf0e Таймлайн ТГУ: по умолчанию только актуальная цепочка планов
Раньше таймлайн рисовал планы всех статусов, включая историю версий
(SUPERSEDED/EXPIRED/REJECTED), из-за чего активная цепочка тонула в
наложении старых планов. Теперь по умолчанию показываются только
актуальные планы (PLANNED/ISSUING/WAITING_DECISION/ACCEPTED), а историю
версий можно включить чекбоксом над таймлайном.
2026-06-02 11:25:58 +03:00
Дмитрий Соловьев e8f253ac6a Гарантия строгого нахлёста планов без разрывов в pcp-tgu-service
Старт следующего плана всегда выбирается строго внутри текущего плана
(нахлёст по времени), а раньше был fallback на ближайший более поздний
старт, который мог дать разрыв. Если ни одна ЗРВ не начинается внутри
текущего плана — цепочка обрывается, но разрыв между планами не создаётся.
calculatePlansAfter тоже стартует хвост строго внутри принятого плана.

Инвариант цепочки в тестах усилен проверкой строгого нахлёста; добавлен
тест на провал видимости.
2026-06-02 11:18:02 +03:00
Дмитрий Соловьев bd59204fcf Незакоммиченная работа: UI заявок/планирования и правки сервисов
- pcp-tgu-ops-ui: вкладка «Заявки», редактор планирования, утилиты uuid
  и e2e-verify скрипты, обновление зависимостей
- pcp-ballistics-service, pcp-mission-planing-service: текущие правки
2026-06-02 11:04:45 +03:00
Дмитрий Соловьев 96f698112b Запрет вложенных планов: строго монотонная цепочка в pcp-tgu-service
Цепочка планов теперь строит соседние планы со строго возрастающими
стартом и концом, поэтому ни один план не может лежать целиком внутри
другого. Нахлёст «на одну ЗРВ» считается по времени, а не по плоскому
индексу кандидата — это убирает вложенность при нескольких станциях,
видящих один пролёт одновременно (кейс Emissio).

Подчистка устаревших PLANNED-планов при rebuild переведена на условие
end_time > now, чтобы гасить и уже стартовавшие вложенные планы.
2026-06-02 11:04:38 +03:00
Дмитрий Соловьев edd24d93ba Игнорировать корневой node_modules (кэш Vite) 2026-06-01 22:19:21 +03:00
Дмитрий Соловьев 0128c5569c 2D-карта (станции, заявки) и вкладка «Заявки»
Новая вкладка «Заявки»: показ заявок за интервал на 2D-карте,
создание заявки рисованием полигона на карте с формой параметров
(Оптика/РСА) и удаление выбранной заявки (createRequest/deleteRequest,
RequestCreatePanel/RequestDetailsPanel, polygonToWkt). В Tgu2DMapView
добавлен изолированный режим рисования полигона.

2D-карта: панели информации о станции и о заявках, слой станций
(stationsApi), поддержка выбора объектов через спатиальный индекс.
Список слоёв упрощён — трасса/полоса/маркеры КА убраны из переключателя,
по умолчанию включены работы плана, станции и заявки.
2026-06-01 22:17:50 +03:00
Дмитрий Соловьев 715b5108d6 Редактор: ручное добавление маршрутов съёмки и сбросов
Режим «Сброс» теперь рабочий: загружает ЗРВ станций
(ballisticsApi.fetchSatelliteRva), даёт оператору выбрать зону, затем
показывает съёмки плана, заканчивающиеся строго до начала зоны, и
сохраняет сброс в плане (missionPlanningApi.saveDropRoute → DropPanel).
Старый фейковый DownlinkPanel на stationPasses убран из потока.
Режим «Съёмка» сохраняет маршрут через saveSurveyRoute.

Времена ЗРВ приходят как LocalDateTime без зоны и трактуются как UTC
(helper localDateTimeMs), чтобы сравнивать с временами съёмок.
В vite-прокси добавлены pcp-ballistics, pcp-mission и stations.
2026-06-01 22:17:32 +03:00
Дмитрий Соловьев ef97ebbcf7 Ручное сохранение маршрутов съёмки и сбросов в плане
Эндпоинты по id выбранного плана (= mission_id):
- POST /api/missions/{plan_id}/survey-route — строит контур и сохраняет
  SurveyModeEntity (source=MANUAL, status=PLANNED);
- POST /api/missions/{plan_id}/drop-route — ручной сброс по выбранной
  зоне радиовидимости: валидирует, что съёмки принадлежат плану и
  заканчиваются строго до начала зоны, а сумма длительностей не
  превышает длительность зоны; пишет DropModeEntity + связи
  DropModeSurveyEntity.

Миссия/план (planNumber=1) создаются на лету, если их ещё нет, с
интервалом из planStart/planEnd запроса. loadSlots чистит только
пересчитываемые маршруты (deleteRecalculableSurveyModesByPlanId),
поэтому ручные маршруты съёмки переживают пересчёт. Новые DTO —
DropRouteSaveRequestDTO и SurveyRouteBuildDTOs в pcp-types-lib.
2026-06-01 22:17:20 +03:00
Дмитрий Соловьев fafaeed418 ЗРВ по реальным станциям приёма
Баллистика теперь сама берёт каталог станций у pcp-stations-service
(новый StationCatalogClient, settings.stations-service, GET /api/stations)
и строит ЗРВ по каждой реальной станции: PPI с ppiId == station.number,
поэтому RadioVisibilityAreaDTO.stationId совпадает с номером станции.
Раньше getRVA использовал одну захардкоженную фейковую станцию.

Заодно исправляет автоматический расчёт сбросов (calculateDrops),
который ходит в getRVA, и заполняет duration зоны.
2026-06-01 22:17:01 +03:00
emelianov 78887dd63a fix requests 2026-06-01 14:22:36 +03:00
Дмитрий Соловьев 2bdc9ad64f Корректное отображение витков 2026-06-01 13:57:13 +03:00
Дмитрий Соловьев e5fd2a4586 chore: ignore pcp tgu ops ui local artifacts 2026-06-01 12:10:23 +03:00
Дмитрий Соловьев 02e172479f Merge branch 'route-processing' into 'dev'
Новый сервис (веб-интерфейс) + рефакторинг

See merge request 2-6/2-6-4/missionis/observatio-terrae!1
2026-06-01 07:55:23 +00:00
Дмитрий Соловьев 34c988c2d0 Конфигурация как в dev 2026-06-01 10:27:05 +03:00
Дмитрий Соловьев bc3dd8f0b8 Конфигурация как в dev 2026-06-01 10:16:41 +03:00
Дмитрий Соловьев ed33c3c834 fix(config): align all infrastructure addresses with dev branch
Replace 192.168.1.8 (local dev machine) with addresses from dev branch:
- Test application.yaml files: kafka/postgres → 192.168.60.68, service URLs → localhost
- pcp-complex-mission main: local profile postgres host → localhost, dev profile → 192.168.100.160:35400
- config-repo/pcp-tgu-service.yaml: camunda grpc-address → camunda.k8s.265.nstart.local:30901
- CorsFilterComponent.kt: allowed origins → 192.168.60.68
- CoverageSchemeProperties.kt: service URLs → localhost
- KafkaProducerConfig.kt: bootstrap servers fallback → 192.168.100.160:19092

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:15:39 +03:00
Дмитрий Соловьев 0ec8c304b4 fix(config): restore parametric datasource URLs after merge from dev
Dev branch had changed datasource URLs to env vars (SPRING_DATASOURCE_URL etc.)
for k8s deployments, but route-processing uses parametric config-server URLs
for local development.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 09:54:23 +03:00
Дмитрий Соловьев 58d067c36f Merge branch 'dev' into route-processing
Conflict resolution:
- build.gradle.kts: dev version (keep nstart.cloud repo + jacoco/asm mavenCentral)
- settings.gradle.kts: project name from route-processing (observatio-terrae), rest from dev
- config addresses (application-local, pcp-mission-planing-service, pcp-dynamic-plan application.yaml): dev versions
- menu.html: merged both branches (TGU planning + bookings items)
- CatalogControllerTest.kt: merged both mocks (TguPlanningService + BallisticsService)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 09:19:29 +03:00
Дмитрий Соловьев da753b4ca9 fix(tgu-ops-ui): fix antimeridian wrapping for request and swath polygons on 2D map
Polygons crossing the antimeridian were rendered with a large visual jump.
Now each polygon is drawn three times (dx = 0, ±worldSize) after unwrapping
X-coordinates, with viewport culling to skip invisible copies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 08:41:49 +03:00
Дмитрий Соловьев d7dda1e2f5 feat(tgu-ops-ui): add pass selector styles and update architecture docs
tgu-editor.css: add .map-pass-selector styles — floating panel with
semi-transparent background, range slider track, fill, tick marks, draggable
thumbs, and per-handle labels showing orbit number + UTC time.

docs: update PCP_ARCHITECTURE.md to reflect pcp-tgu-ops-ui is now a real SPA
(not a placeholder); add PCP_TGU_OPS_UI_CAPABILITY_MAP.md describing feature
areas and data sources for the TGU ops frontend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 16:08:30 +03:00
Дмитрий Соловьев 6d1b8e7cde fix(tgu-ops-ui): fix track antimeridian wrap and polygon culling on 2D map
drawTracks: detect antimeridian crossing by comparing consecutive screen X
positions against worldSize/2; use moveTo instead of lineTo on large jumps
so tracks don't draw a horizontal line across the globe.

Tgu2DMapView: replace the heuristic lon-range check (> 300°) with an exact
condition — skip bbox culling when the canvas is wider than the world tile
(entire globe visible) or when topLeft.lon >= bottomRight.lon (viewport
straddles the antimeridian). Both cases previously produced a nonsensical
bbox that hid polygons near ±180°.

drawRequests: remove per-polygon screen-space culling; viewport filtering is
already handled upstream by visiblePolygons, so the redundant check was
incorrectly hiding requests at high zoom levels near canvas edges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 16:08:20 +03:00
Дмитрий Соловьев 4ab765b0ee feat(tgu-ops-ui): display real ground track and swath from ballistics service
Replace synthetic orbit calculation (Kepler + spacecraft ID hash) with real
data from pcp-ballistics-service via a new proxy endpoint in pcp-ui-service.

Backend:
- TguPlanningController: add GET /api/tgu-planning/spacecraft/{noradId}/flight-line
  that proxies FlightLineDTO[] from BallisticsService for a given time interval

Frontend:
- Fetch FlightLineDTO[] (revolution, time, lat/long, swath boundaries) using
  the spacecraft's noradId for the current 24h map window
- Group points by real revolution number to build OrbitPassInfo[] with actual
  orbit numbers and UTC times instead of synthetic Kepler-based estimates
- Build ground track MapLines and swath MapPolygons from real coordinates;
  swath polygon = outer-left edge forward + outer-right edge reversed
- Enable swath layer in the editor map; draw with teal fill/stroke matching
  the track colour
- MapPassSelector now shows real revolution numbers in header and handle labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 16:05:41 +03:00
Дмитрий Соловьев 637a5e8848 feat(tgu-ops-ui): display plan works and new shoots on editor map
На вкладке Редактор 2D-карта теперь показывает включения выбранного плана
и новые съёмки, добавленные в текущей сессии.

- editorApi: типы PlanSurveyMode/PlanDropMode/PlanMode и fetchPlanModes —
  вызывает GET /api/current-plans/missions/{planId}/modes на pcp-mission-planing-service,
  где planId TGU совпадает с missionId (выяснено по Camunda-процессу)
- TguEditorTab: загружает включения при выборе плана, строит MapPolygon[]
  из SURVEY-режимов через parseWktPolygon(contourWkt), включён слой planWorks
- Tgu2DMapView: разделены pending-маркер (оранжевый, выбор точки)
  и confirmed-маркеры (бирюзовые, подтверждённые съёмки из драфта);
  клик на маркер съёмки открывает WorkInspector

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 09:18:53 +03:00
Дмитрий Соловьев 42708450bb feat(tgu-ops-ui): display request polygons on 2D map
Добавлен слой заявок (КНС) на 2D карту ТГУ:
- requestApi.ts — загрузка заявок постранично через GET /v1/requests/map
  с прогресс-коллбэком; автоматически собирает все страницы
- wktParser.ts — парсинг WKT POLYGON/MULTIPOLYGON в массив координат
- drawRequests.ts — Canvas-рендеринг полигонов заявок
- Tgu2DMapLayers.ts — buildTgu2DMapScene строит MapPolygon[] из RequestMapItem[]
- Tgu2DMapTab.tsx — состояние requestsState, кнопка «Загрузить», фильтры по дате
- Tgu2DMapSidebar.tsx — панель слоя заявок: счётчик, ошибки, truncated-предупреждение
- requestTypes.ts — RequestMapItem, RequestMapPage модели
- Поддержка выбора заявки на карте (mapSelectionTypes, mapTypes)
- nginx.conf, vite.config.ts — проксирование /v1 на pcp-request-service в dev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:28:58 +03:00
Дмитрий Соловьев bb84844674 refactor(pcp-request-service): rename REST endpoints and DTOs for clarity
- GET /v1/requests/with-geometry → /v1/requests/map
- GET /v1/requests/{id}/with-cells → /v1/requests/{id}/cells
- GET /v1/cells/with-requests → /v1/cells/priority-map
- CreateRequestRequestDto → CreateRequestDto
- ListRequestsQueryDto → RequestListFilter
- RequestWithGeometry* → RequestMap* (RequestMapItemDto, RequestMapPageDto)
- CellsWithRequestsResponseDto → CellPriorityMapResponseDto
- DeleteRequestStatusDto removed; status field removed from DeleteRequestResponseDto

Updated all consumers (pcp-complex-mission-service, pcp-ui-service, slots-service),
tests, and OpenAPI YAML.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:28:45 +03:00
Дмитрий Соловьев 3d508b6be9 Update TGU 2D map spacecraft visibility controls
Switch the 2D map sidebar from single-spacecraft selection to per-spacecraft visibility toggles, add a show-all action, and update map tab labels and styles for the all-spacecraft view.
2026-05-30 21:13:50 +03:00
Дмитрий Соловьев 39f108e496 Handle Ordinis outage in TGU service
Classify Ordinis classifier transport failures and empty responses as unavailable, keep stale platform cache when possible, and expose Prometheus metrics/alerting for the outage signal.
2026-05-30 21:04:57 +03:00
Дмитрий Соловьев 82eddedc92 feat(tgu-ops-ui): OSM tiles, editor layout fixes, map resizer
Карта:
- Заменить схематичные контуры континентов на тайлы CartoDB Dark
  (dark_nolabels, CC BY 3.0, без API-ключа) через модульный кеш
  tileLoader.ts + отрисовщик drawTiles.ts на существующей Web Mercator
  проекции — без новых зависимостей
- Сохранить сетку lat/lon 30° поверх тайлов (полезна для орбитального
  анализа), убрать нарисованные контуры и градиент океана
- Обновить атрибуцию: © OpenStreetMap contributors · © CARTO

Вкладка Редактор — раскладка:
- Исправить цепочку высот: workspace переведён с display:grid на
  display:flex + flex-direction:column; layout получает flex:1 — единственный
  надёжный способ пробросить definite height через несколько вложенных
  grid-контейнеров в Chrome
- Обернуть условные notice/applyError в постоянный div.tgu-editor-notices,
  чтобы .tgu-editor-layout всегда был третьим flex-ребёнком (раньше при
  отсутствии уведомлений попадал во вторую auto-строку grid и схлопывался)

Вкладка Редактор — карта:
- Добавить перетаскиваемый разделитель между картой и таймлайном:
  mapRatio state (по умолчанию 50/50), drag-хендлер через document events,
  ограничение 15–80%, визуальный индикатор .tgu-editor-map-resizer
- Ограничить окно наземной трассы 24ч (720 точек, шаг 2 мин) независимо
  от зума таймлайна — при широком диапазоне (7 суток) шаг был 38 мин,
  трасса выглядела как хаотическая сетка через весь глобус

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:30:53 +03:00
Дмитрий Соловьев f084775cbb feat(tgu-ops-ui): add spacecraft work editor tab
Port the TGU editor prototype into pcp-tgu-ops-ui as a new editor tab, add draft/conflict/pass logic with tests, extend the 2D map picking API, and include editor styles/theme aliases and task/prototype docs.

Validation: npm run test; npm run build in services/pcp-tgu-ops-ui.
2026-05-30 15:28:34 +03:00
Дмитрий Соловьев c3a1a8b4a1 Add PCP architecture docs and TGU ops updates
Capture current PCP architecture notes, service-map prototypes, TGU operations UI/map work, local configuration updates, database helper scripts, and request/sample JSON artifacts.
2026-05-30 14:18:19 +03:00
Дмитрий Соловьев 9fca4d4051 pcp-tgu интерфейс 2026-05-29 16:09:36 +03:00
emelianov 24af140213 БД для stations dev 2026-05-29 15:47:49 +03:00
emelianov 651c85d64c БД для requests dev 2026-05-29 15:24:58 +03:00
emelianov 723ceead32 БД для requests dev 2026-05-29 15:18:57 +03:00
Дмитрий Соловьев 913b3d7c9b pcp-tgu интерфейс 2026-05-29 15:18:21 +03:00
Дмитрий Соловьев 4556592e63 pcp-tgu интерфейс 2026-05-29 15:15:02 +03:00
emelianov eff24a254d БД для requests dev 2026-05-29 14:55:29 +03:00
emelianov 366ed8f451 БД для slots dev 2026-05-29 14:50:27 +03:00
emelianov 2d100fdd54 БД для complex-missions dev 2026-05-29 14:45:24 +03:00
emelianov 41c3539b17 БД для ballistics dev 2026-05-29 14:30:03 +03:00
emelianov a6f179550c БД для ballistics dev 2026-05-29 14:07:15 +03:00
Дмитрий Соловьев e7c2060e2e pcp-tgu 2026-05-29 13:35:32 +03:00
emelianov 3fd702ca47 Задание НУ из ui 2026-05-29 12:41:32 +03:00
emelianov d693bf799d Задание НУ из ui 2026-05-29 10:52:36 +03:00
emelianov 7dfa1163e9 Расчет средних ЗРВ 2026-05-28 16:47:43 +03:00
emelianov 2c65f412cf Расчет средних ЗРВ 2026-05-28 13:49:35 +03:00
emelianov 6e62a0bd19 -sonar 2026-05-27 17:55:03 +03:00
emelianov 7dc786362c -sonar 2026-05-27 17:09:47 +03:00
emelianov 5c3f145267 -sonar 2026-05-27 15:58:30 +03:00
emelianov b4dda51460 -sonar 2026-05-27 15:52:14 +03:00
emelianov c7f3a77ff1 анализ TLE 2026-05-27 15:34:34 +03:00
emelianov c95d5239ec анализ TLE 2026-05-27 15:33:12 +03:00
emelianov bfb9d5b01f анализ TLE 2026-05-27 15:30:24 +03:00
emelianov a7e950a131 сравнение TLE 2026-05-27 13:03:31 +03:00
emelianov da1217cbe8 сравнение TLE 2026-05-27 12:53:07 +03:00
emelianov db9e995d46 сравнение TLE 2026-05-27 12:50:37 +03:00
emelianov b846317f47 сравнение TLE 2026-05-27 12:45:26 +03:00
emelianov d07b17e03e сравнение TLE 2026-05-27 12:35:20 +03:00
emelianov f4014a08a1 сравнение TLE 2026-05-27 12:00:52 +03:00
emelianov 29c06a4c9c сравнение TLE 2026-05-27 11:40:31 +03:00
Дмитрий Соловьев 3dce0aec0d Основные изменения:
состояние окон видимости, расписания планов, попыток выдачи и job’ов перенесено в БД;
план и попытка выдачи разделены на разные сущности;
Kafka-события теперь двигают persisted state/jobs, а не локальную память pod’а;
BPMN createSatelliteMission теперь отвечает только за одну попытку выдачи плана;
обновление слотов после принятия плана вынесено в отдельный BPMN-процесс;
workers используют DB claim/lock model и безопасны для нескольких pod’ов;
тестовый ручной API вынесен в отдельный disabled-by-default controller
2026-05-27 10:29:41 +03:00
emelianov 1a6e51bf25 Учет забронированных слотов при расчете комплексного плана 2026-05-27 10:24:14 +03:00
emelianov 2aafa561ed Учет забронированных слотов при расчете комплексного плана 2026-05-27 10:03:23 +03:00
Дмитрий Соловьев cf3bb63bc1 Запрос только аппаратов со статусом operational 2026-05-26 14:03:59 +03:00
Дмитрий Соловьев 12d8b01bae При отсутствии данных endpoint возвращает пустой лист 2026-05-26 13:44:38 +03:00
emelianov 4d2a5952c9 test 2026-05-26 13:09:39 +03:00
emelianov 23c540ead0 test 2026-05-26 12:55:21 +03:00
emelianov 467f91a374 Доработка отображения текущего плана 2026-05-26 12:53:04 +03:00
Дмитрий Соловьев 4edf9d7aa8 Добавлен кэш, запросы в НСИ раз в 15 минут 2026-05-26 12:41:08 +03:00
emelianov 38c1449e97 Доработка отображения текущего плана 2026-05-26 12:32:33 +03:00
Дмитрий Соловьев cbbafebf79 Перевод pcp-tgu на новый НСИ 2026-05-26 11:07:37 +03:00
emelianov 1229995f90 Доработка отображения текущего плана 2026-05-25 22:02:17 +03:00
emelianov d4682fdeb7 отображение забронированных слотов и тестовые НУ 2026-05-25 21:02:53 +03:00
emelianov f549e15c63 отображение забронированных слотов и тестовые НУ 2026-05-25 21:02:50 +03:00
Дмитрий Соловьев 1ef912be66 @ 2026-05-25 15:57:11 +03:00
Дмитрий Соловьев fa357a78b4 улучшить observability и документацию outbox
- обновить README с текущим Kafka, DB, outbox и scaling flow
- расширить логи listener, ingestion, publisher и outbox worker
- не логировать raw payload и outbox payload
- закрыть test controller явным property
- добавить Micrometer counters для ingestion и outbox outcomes
- сохранить listener flow, payload contract и delivery semantics без изменений
2026-05-25 15:20:25 +03:00
Дмитрий Соловьев 4073897efc добавить масштабируемый outbox worker для паспортов маршрутов
- добавить properties и config values для route passport outbox worker
- включить Spring scheduling в route-processing service
- добавить транзакционный сервис claim и status transitions для outbox
- публиковать claimed outbox rows через существующий Kafka publisher
- обрабатывать publish failures по каждой row с bounded exponential backoff
- обеспечить безопасность при нескольких pod через FOR UPDATE SKIP LOCKED и lockedUntil
- добавить unit tests для claim, publish transitions, retry/fail behavior и batch-обработки worker
2026-05-25 15:05:10 +03:00
Дмитрий Соловьев b5990a55ac добавить масштабируемый outbox worker для паспортов маршрутов
- добавить properties и config values для route passport outbox worker
- включить Spring scheduling в route-processing service
- добавить транзакционный сервис claim и status transitions для outbox
- публиковать claimed outbox rows через существующий Kafka publisher
- обрабатывать publish failures по каждой row с bounded exponential backoff
- обеспечить безопасность при нескольких pod через FOR UPDATE SKIP LOCKED и lockedUntil
- добавить unit tests для claim, publish transitions, retry/fail behavior и batch-обработки worker
2026-05-25 15:05:04 +03:00
Дмитрий Соловьев 9d6f838692 публикация одной outbox-записи паспорта маршрута
- добавить надёжные настройки Kafka producer
- публиковать один record из RoutePassportOutboxEntity с timeout
- сериализовать JsonNode payload напрямую как JSON object
- пробрасывать ошибки Kafka send вызывающему коду
- обновить тесты publisher и producer config
2026-05-25 14:52:26 +03:00
Дмитрий Соловьев d48ddd2657 Init 2026-05-25 14:23:52 +03:00
1547 changed files with 172743 additions and 3 deletions
+6
View File
@@ -0,0 +1,6 @@
.git
.gradle
.idea
**/build
out
*.iml
+3
View File
@@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary
+79
View File
@@ -0,0 +1,79 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
*.tmp
*.log
*.gz
*.zip
*.patch
.gradle-local/
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin
### Local env ###
.env
/.env.example
/pcp.zip
/promt/Промт1.txt
/pcp_zip.zip
/pcp-chatgpt-context*.zip
/deploy/local-bundle/build/
/deploy/local-bundle/.env
/deploy/local-bundle/pcp-local-bundle-*.tar.gz
### Node / Vite ###
/package-lock.json
/node_modules/
/services/pcp-tgu-ops-ui/node_modules/
/services/pcp-tgu-ops-ui/dist/
/services/pcp-tgu-ops-ui/.cache/
/services/pcp-tgu-ops-ui/.vite/
/services/pcp-tgu-ops-ui/.vitest/
/services/pcp-tgu-ops-ui/coverage/
/services/pcp-tgu-ops-ui/.nyc_output/
/services/pcp-tgu-ops-ui/.eslintcache
/services/pcp-tgu-ops-ui/npm-debug.log*
/services/pcp-tgu-ops-ui/yarn-debug.log*
/services/pcp-tgu-ops-ui/yarn-error.log*
/services/pcp-tgu-ops-ui/pnpm-debug.log*
.gstack/
# Локальная раскладка собранных bootJar всех сервисов (артефакты, ~1.7G) — не версионируем.
/serviceAllBuild/
# Машинно-зависимый npm prefix фронта (локальный путь разработчика).
/services/pcp-tgu-ui-service/.npmrc
+177
View File
@@ -0,0 +1,177 @@
workflow:
rules:
# Аварийный рубильник: «Run pipeline» c переменной RUN_ALL=true прогоняет
# все gradle-сервисы (кроме pcp-tgu-ui-service) независимо от изменений.
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- services/pcp-ballistics-service/**/*
- services/pcp-srpring-boot-admin-server/**/*
- services/spring-cloud-config-server/**/*
- services/pcp-complex-mission-service/**/*
- services/tle-monitoring-service/**/*
- services/pcp-mission-planing-service/**/*
- services/pcp-request-service/**/*
- services/pcp-stations-service/**/*
- services/pcp-satellite-catalog-service/**/*
- services/slots-service/**/*
- services/pcp-ui-service/**/*
- services/pcp-coverage-scheme-service/**/*
- services/pcp-tgu-service/**/*
- services/pcp-tgu-ui-service/**/*
- services/pcp-dynamic-plan-service/**/*
- services/pcp-gateway-service/**/*
- libs/pcp-types-lib/**/*
- libs/ballistics-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- gradle.properties
- gradle/**/*
- .gitlab-ci.yml
- .gitlab/ci/**/*
- helm/pcp-ballistics-service/**/*
- helm/pcp-spring-boot-admin-server/**/*
- helm/spring-cloud-config-server/**/*
- helm/pcp-complex-mission-service/**/*
- helm/tle-monitoring-service/**/*
- helm/pcp-mission-planing-service/**/*
- helm/pcp-request-service/**/*
- helm/pcp-stations-service/**/*
- helm/pcp-satellite-catalog-service/**/*
- helm/slots-service/**/*
- helm/pcp-ui-service/**/*
- helm/pcp-coverage-scheme-service/**/*
- helm/pcp-tgu-service/**/*
- helm/pcp-tgu-ui-service/**/*
- helm/pcp-dynamic-plan-service/**/*
- helm/pcp-gateway-service/**/*
- if: '$CI_PIPELINE_SOURCE == "push"'
changes:
- .gitlab-ci.yml
- .gitlab/ci/**/*
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
changes:
- services/pcp-ballistics-service/**/*
- services/pcp-srpring-boot-admin-server/**/*
- services/spring-cloud-config-server/**/*
- services/pcp-complex-mission-service/**/*
- services/tle-monitoring-service/**/*
- services/pcp-mission-planing-service/**/*
- services/pcp-request-service/**/*
- services/pcp-stations-service/**/*
- services/pcp-satellite-catalog-service/**/*
- services/slots-service/**/*
- services/pcp-ui-service/**/*
- services/pcp-coverage-scheme-service/**/*
- services/pcp-tgu-service/**/*
- services/pcp-tgu-ui-service/**/*
- services/pcp-dynamic-plan-service/**/*
- services/pcp-gateway-service/**/*
- libs/pcp-types-lib/**/*
- libs/ballistics-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- gradle.properties
- gradle/**/*
- .gitlab-ci.yml
- .gitlab/ci/**/*
- helm/pcp-ballistics-service/**/*
- helm/pcp-spring-boot-admin-server/**/*
- helm/spring-cloud-config-server/**/*
- helm/pcp-complex-mission-service/**/*
- helm/tle-monitoring-service/**/*
- helm/pcp-mission-planing-service/**/*
- helm/pcp-request-service/**/*
- helm/pcp-stations-service/**/*
- helm/pcp-satellite-catalog-service/**/*
- helm/slots-service/**/*
- helm/pcp-ui-service/**/*
- helm/pcp-coverage-scheme-service/**/*
- helm/pcp-tgu-service/**/*
- helm/pcp-tgu-ui-service/**/*
- helm/pcp-dynamic-plan-service/**/*
- helm/pcp-gateway-service/**/*
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"'
changes:
- services/pcp-ballistics-service/**/*
- services/pcp-srpring-boot-admin-server/**/*
- services/spring-cloud-config-server/**/*
- services/pcp-complex-mission-service/**/*
- services/tle-monitoring-service/**/*
- services/pcp-mission-planing-service/**/*
- services/pcp-request-service/**/*
- services/pcp-stations-service/**/*
- services/pcp-satellite-catalog-service/**/*
- services/slots-service/**/*
- services/pcp-ui-service/**/*
- services/pcp-coverage-scheme-service/**/*
- services/pcp-tgu-service/**/*
- services/pcp-tgu-ui-service/**/*
- services/pcp-dynamic-plan-service/**/*
- services/pcp-gateway-service/**/*
- libs/pcp-types-lib/**/*
- libs/ballistics-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- gradle.properties
- gradle/**/*
- .gitlab-ci.yml
- .gitlab/ci/**/*
- helm/pcp-ballistics-service/**/*
- helm/pcp-spring-boot-admin-server/**/*
- helm/spring-cloud-config-server/**/*
- helm/pcp-complex-mission-service/**/*
- helm/tle-monitoring-service/**/*
- helm/pcp-mission-planing-service/**/*
- helm/pcp-request-service/**/*
- helm/pcp-stations-service/**/*
- helm/pcp-satellite-catalog-service/**/*
- helm/slots-service/**/*
- helm/pcp-ui-service/**/*
- helm/pcp-coverage-scheme-service/**/*
- helm/pcp-tgu-service/**/*
- helm/pcp-tgu-ui-service/**/*
- helm/pcp-dynamic-plan-service/**/*
- helm/pcp-gateway-service/**/*
- when: never
default:
tags:
- nstart
variables:
INTERNAL_REGISTRY: "$CI_REGISTRY_IMAGE"
stages:
- test
- build
- package
- deploy
ci:config:
stage: test
image: alpine:3.20
rules:
- changes:
- .gitlab-ci.yml
- .gitlab/ci/**/*
- when: never
script:
- echo "GitLab CI configuration changed"
include:
- local: .gitlab/ci/pcp-ballistics-service.yml
- local: .gitlab/ci/pcp-spring-boot-admin-server.yml
- local: .gitlab/ci/spring-cloud-config-server.yml
- local: .gitlab/ci/pcp-complex-mission-service-tle.yml
- local: .gitlab/ci/pcp-mission-planing-service.yml
- local: .gitlab/ci/pcp-request-service.yml
- local: .gitlab/ci/pcp-stations-service.yml
- local: .gitlab/ci/pcp-satellite-catalog-service.yml
- local: .gitlab/ci/slots-service.yml
- local: .gitlab/ci/pcp-ui-service.yml
- local: .gitlab/ci/pcp-coverage-scheme-service.yml
- local: .gitlab/ci/pcp-tgu-service.yml
- local: .gitlab/ci/pcp-tgu-ui-service.yml
- local: .gitlab/ci/pcp-dynamic-plan-service.yml
- local: .gitlab/ci/pcp-gateway-service.yml
+267
View File
@@ -0,0 +1,267 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
BALLISTICS_SERVICE_DIR: "services/pcp-ballistics-service"
# При необходимости переопредели путь к chart через CI/CD variable
BALLISTICS_HELM_CHART_DIR: "helm/pcp-ballistics-service"
# Имя релиза в k8s
BALLISTICS_HELM_RELEASE: "pcp-ballistics-service"
# Полный путь до image во внутреннем registry
BALLISTICS_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-ballistics-service"
# Окружения / namespace
BALLISTICS_K8S_NAMESPACE_DEV: "pcp-dev"
BALLISTICS_K8S_NAMESPACE_MASTER: "pcp-master"
BALLISTICS_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
.ballistics:changes: &ballistics_changes
- services/pcp-ballistics-service/**/*
- libs/ballistics-lib/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-ballistics-service.*
- helm/pcp-ballistics-service/**/*
- deploy/**/*
.ballistics:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *ballistics_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *ballistics_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *ballistics_changes
- when: never
.ballistics:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *ballistics_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *ballistics_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *ballistics_changes
- when: never
.ballistics:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *ballistics_changes
when: manual
- when: never
.ballistics:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *ballistics_changes
when: manual
- when: never
.ballistics:gradle-cache:
cache:
key: "gradle-ballistics-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.ballistics:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.ballistics:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
ballistics:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
services:
- name: repo.nstart.cloud/postgis/postgis:16-3.4
alias: postgres
variables:
POSTGRES_DB: "pcp_ballistics"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "password"
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/pcp_ballistics"
SPRING_DATASOURCE_USERNAME: "postgres"
SPRING_DATASOURCE_PASSWORD: "password"
extends:
- .ballistics:rules:test
- .ballistics:gradle-cache
script:
- gradle --no-daemon :services:pcp-ballistics-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-ballistics-service/build/test-results/test/*.xml
paths:
- services/pcp-ballistics-service/build/reports/tests/
expire_in: 7 days
ballistics:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .ballistics:rules:build-and-image
- .ballistics:gradle-cache
needs:
- job: ballistics:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-ballistics-service:bootJar
artifacts:
when: always
paths:
- services/pcp-ballistics-service/build/libs/*.jar
expire_in: 7 days
ballistics:package:
stage: package
extends:
- .ballistics:rules:build-and-image
- .ballistics:docker-build
needs:
- job: ballistics:build
artifacts: true
script:
- 'echo "BALLISTICS_IMAGE_REPOSITORY=$BALLISTICS_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$BALLISTICS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$BALLISTICS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$BALLISTICS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$BALLISTICS_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$BALLISTICS_IMAGE_REPOSITORY:$BALLISTICS_IMAGE_TAG" docker-context
- docker push "$BALLISTICS_IMAGE_REPOSITORY:$BALLISTICS_IMAGE_TAG"
- printf "BALLISTICS_IMAGE=%s\n" "$BALLISTICS_IMAGE_REPOSITORY:$BALLISTICS_IMAGE_TAG" > ballistics-image.env
artifacts:
reports:
dotenv: ballistics-image.env
expire_in: 7 days
ballistics:deploy:dev:
stage: deploy
extends:
- .ballistics:rules:deploy-dev
- .ballistics:helm-deploy
needs:
- job: ballistics:package
artifacts: true
environment:
name: dev/pcp-ballistics-service
resource_group: pcp-ballistics-service-dev
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${BALLISTICS_IMAGE%:*}"'
- 'export IMAGE_TAG="${BALLISTICS_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$BALLISTICS_HELM_RELEASE"
"$BALLISTICS_HELM_CHART_DIR"
--namespace "$BALLISTICS_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$BALLISTICS_HELM_CHART_DIR/values.yaml"
-f "$BALLISTICS_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
ballistics:deploy:master:
stage: deploy
extends:
- .ballistics:rules:deploy-master
- .ballistics:helm-deploy
needs:
- job: ballistics:package
artifacts: true
environment:
name: master/pcp-ballistics-service
resource_group: pcp-ballistics-service-master
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${BALLISTICS_IMAGE%:*}"'
- 'export IMAGE_TAG="${BALLISTICS_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$BALLISTICS_HELM_RELEASE"
"$BALLISTICS_HELM_CHART_DIR"
--namespace "$BALLISTICS_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$BALLISTICS_HELM_CHART_DIR/values.yaml"
-f "$BALLISTICS_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
@@ -0,0 +1,471 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
COMPLEX_MISSION_SERVICE_DIR: "services/pcp-complex-mission-service"
COMPLEX_MISSION_SERVICE_HELM_CHART_DIR: "helm/pcp-complex-mission-service"
COMPLEX_MISSION_SERVICE_HELM_RELEASE: "pcp-complex-mission-service"
COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-complex-mission-service"
COMPLEX_MISSION_SERVICE_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
COMPLEX_MISSION_SERVICE_K8S_NAMESPACE_DEV: "pcp-dev"
COMPLEX_MISSION_SERVICE_K8S_NAMESPACE_MASTER: "pcp-master"
TLE_MONITORING_SERVICE_DIR: "services/tle-monitoring-service"
TLE_MONITORING_HELM_CHART_DIR: "helm/tle-monitoring-service"
TLE_MONITORING_HELM_RELEASE: "tle-monitoring-service"
TLE_MONITORING_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/tle-monitoring-service"
TLE_MONITORING_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
TLE_MONITORING_K8S_NAMESPACE_DEV: "pcp-dev"
TLE_MONITORING_K8S_NAMESPACE_MASTER: "pcp-master"
.complex_mission_service_changes: &complex_mission_service_changes
- services/pcp-complex-mission-service/**/*
- libs/ballistics-lib/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-complex-mission-service-tle.yml
- helm/pcp-complex-mission-service/**/*
- deploy/**/*
.tle_monitoring_changes: &tle_monitoring_changes
- services/tle-monitoring-service/**/*
- libs/ballistics-lib/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-complex-mission-service-tle.yml
- helm/tle-monitoring-service/**/*
- deploy/**/*
.complex-mission-service:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *complex_mission_service_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *complex_mission_service_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *complex_mission_service_changes
- when: never
.complex-mission-service:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *complex_mission_service_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *complex_mission_service_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *complex_mission_service_changes
- when: never
.complex-mission-service:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *complex_mission_service_changes
when: manual
- when: never
.complex-mission-service:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *complex_mission_service_changes
when: manual
- when: never
.tle-monitoring:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *tle_monitoring_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *tle_monitoring_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *tle_monitoring_changes
- when: never
.tle-monitoring:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *tle_monitoring_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *tle_monitoring_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *tle_monitoring_changes
- when: never
.tle-monitoring:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *tle_monitoring_changes
when: manual
- when: never
.tle-monitoring:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *tle_monitoring_changes
when: manual
- when: never
.pcp-gradle-cache:
cache:
key: "gradle-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.pcp-docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.pcp-helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube ~/.kube
- helm version
after_script:
- rm -f /root/.kube/config ~/.kube/config
complex-mission-service:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .complex-mission-service:rules:test
- .pcp-gradle-cache
script:
- gradle --no-daemon :services:pcp-complex-mission-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-complex-mission-service/build/test-results/test/*.xml
paths:
- services/pcp-complex-mission-service/build/reports/tests/
expire_in: 7 days
complex-mission-service:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .complex-mission-service:rules:build-and-image
- .pcp-gradle-cache
needs:
- job: complex-mission-service:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-complex-mission-service:bootJar
artifacts:
when: always
paths:
- services/pcp-complex-mission-service/build/libs/*.jar
expire_in: 7 days
complex-mission-service:package:
stage: package
extends:
- .complex-mission-service:rules:build-and-image
- .pcp-docker-build
needs:
- job: complex-mission-service:build
artifacts: true
script:
- 'echo "COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY=$COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$COMPLEX_MISSION_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$COMPLEX_MISSION_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$COMPLEX_MISSION_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$COMPLEX_MISSION_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY:$COMPLEX_MISSION_SERVICE_IMAGE_TAG" docker-context
- docker push "$COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY:$COMPLEX_MISSION_SERVICE_IMAGE_TAG"
- printf "COMPLEX_MISSION_SERVICE_IMAGE=%s\n" "$COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY:$COMPLEX_MISSION_SERVICE_IMAGE_TAG" > complex-mission-service-image.env
artifacts:
reports:
dotenv: complex-mission-service-image.env
expire_in: 7 days
complex-mission-service:deploy:dev:
stage: deploy
extends:
- .complex-mission-service:rules:deploy-dev
- .pcp-helm-deploy
needs:
- job: complex-mission-service:package
artifacts: true
environment:
name: dev/pcp-complex-mission-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${COMPLEX_MISSION_SERVICE_IMAGE%:*}"'
- 'export IMAGE_TAG="${COMPLEX_MISSION_SERVICE_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$COMPLEX_MISSION_SERVICE_HELM_RELEASE"
"$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR"
--namespace "$COMPLEX_MISSION_SERVICE_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR/values.yaml"
-f "$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
complex-mission-service:deploy:master:
stage: deploy
extends:
- .complex-mission-service:rules:deploy-master
- .pcp-helm-deploy
needs:
- job: complex-mission-service:package
artifacts: true
environment:
name: master/pcp-complex-mission-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${COMPLEX_MISSION_SERVICE_IMAGE%:*}"'
- 'export IMAGE_TAG="${COMPLEX_MISSION_SERVICE_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$COMPLEX_MISSION_SERVICE_HELM_RELEASE"
"$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR"
--namespace "$COMPLEX_MISSION_SERVICE_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR/values.yaml"
-f "$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
tle-monitoring:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
services:
- name: repo.nstart.cloud/postgis/postgis:16-3.4
alias: postgres
variables:
POSTGRES_DB: "pcp_tle"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "password"
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/pcp_tle"
SPRING_DATASOURCE_USERNAME: "postgres"
SPRING_DATASOURCE_PASSWORD: "password"
SPRING_TASK_SCHEDULING_ENABLED: "false"
extends:
- .tle-monitoring:rules:test
- .pcp-gradle-cache
script:
- gradle --no-daemon :services:tle-monitoring-service:test
artifacts:
when: always
reports:
junit:
- services/tle-monitoring-service/build/test-results/test/*.xml
paths:
- services/tle-monitoring-service/build/reports/tests/
expire_in: 7 days
tle-monitoring:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .tle-monitoring:rules:build-and-image
- .pcp-gradle-cache
needs:
- job: tle-monitoring:test
artifacts: true
script:
- gradle --no-daemon :services:tle-monitoring-service:bootJar
artifacts:
when: always
paths:
- services/tle-monitoring-service/build/libs/*.jar
expire_in: 7 days
tle-monitoring:package:
stage: package
extends:
- .tle-monitoring:rules:build-and-image
- .pcp-docker-build
needs:
- job: tle-monitoring:build
artifacts: true
script:
- 'echo "TLE_MONITORING_IMAGE_REPOSITORY=$TLE_MONITORING_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$TLE_MONITORING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$TLE_MONITORING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$TLE_MONITORING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$TLE_MONITORING_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$TLE_MONITORING_IMAGE_REPOSITORY:$TLE_MONITORING_IMAGE_TAG" docker-context
- docker push "$TLE_MONITORING_IMAGE_REPOSITORY:$TLE_MONITORING_IMAGE_TAG"
- printf "TLE_MONITORING_IMAGE=%s\n" "$TLE_MONITORING_IMAGE_REPOSITORY:$TLE_MONITORING_IMAGE_TAG" > tle-monitoring-image.env
artifacts:
reports:
dotenv: tle-monitoring-image.env
expire_in: 7 days
tle-monitoring:deploy:dev:
stage: deploy
extends:
- .tle-monitoring:rules:deploy-dev
- .pcp-helm-deploy
needs:
- job: tle-monitoring:package
artifacts: true
environment:
name: dev/tle-monitoring-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${TLE_MONITORING_IMAGE%:*}"'
- 'export IMAGE_TAG="${TLE_MONITORING_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$TLE_MONITORING_HELM_RELEASE"
"$TLE_MONITORING_HELM_CHART_DIR"
--namespace "$TLE_MONITORING_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 15m
-f "$TLE_MONITORING_HELM_CHART_DIR/values.yaml"
-f "$TLE_MONITORING_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
tle-monitoring:deploy:master:
stage: deploy
extends:
- .tle-monitoring:rules:deploy-master
- .pcp-helm-deploy
needs:
- job: tle-monitoring:package
artifacts: true
environment:
name: master/tle-monitoring-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${TLE_MONITORING_IMAGE%:*}"'
- 'export IMAGE_TAG="${TLE_MONITORING_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$TLE_MONITORING_HELM_RELEASE"
"$TLE_MONITORING_HELM_CHART_DIR"
--namespace "$TLE_MONITORING_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 15m
-f "$TLE_MONITORING_HELM_CHART_DIR/values.yaml"
-f "$TLE_MONITORING_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+244
View File
@@ -0,0 +1,244 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
COVERAGE_SCHEME_SERVICE_DIR: "services/pcp-coverage-scheme-service"
COVERAGE_SCHEME_HELM_CHART_DIR: "helm/pcp-coverage-scheme-service"
COVERAGE_SCHEME_HELM_RELEASE: "pcp-coverage-scheme-service"
COVERAGE_SCHEME_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-coverage-scheme-service"
COVERAGE_SCHEME_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
COVERAGE_SCHEME_K8S_NAMESPACE_DEV: "pcp-dev"
COVERAGE_SCHEME_K8S_NAMESPACE_MASTER: "pcp-master"
.coverage_scheme_changes: &coverage_scheme_changes
- services/pcp-coverage-scheme-service/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- gradle.properties
- .gitlab/ci/pcp-coverage-scheme-service.yml
- helm/pcp-coverage-scheme-service/**/*
- deploy/**/*
.coverage_scheme:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *coverage_scheme_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *coverage_scheme_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *coverage_scheme_changes
- when: never
.coverage_scheme:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *coverage_scheme_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *coverage_scheme_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *coverage_scheme_changes
- when: never
.coverage_scheme:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *coverage_scheme_changes
when: manual
- when: never
.coverage_scheme:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *coverage_scheme_changes
when: manual
- when: never
.coverage_scheme:gradle-cache:
cache:
key: "gradle-coverage-scheme-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.coverage_scheme:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.coverage_scheme:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
coverage-scheme:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .coverage_scheme:rules:test
- .coverage_scheme:gradle-cache
script:
- gradle --no-daemon :services:pcp-coverage-scheme-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-coverage-scheme-service/build/test-results/test/*.xml
paths:
- services/pcp-coverage-scheme-service/build/reports/tests/
expire_in: 7 days
coverage-scheme:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .coverage_scheme:rules:build-and-image
- .coverage_scheme:gradle-cache
needs:
- job: coverage-scheme:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-coverage-scheme-service:bootJar
artifacts:
when: always
paths:
- services/pcp-coverage-scheme-service/build/libs/*.jar
expire_in: 7 days
coverage-scheme:package:
stage: package
extends:
- .coverage_scheme:rules:build-and-image
- .coverage_scheme:docker-build
needs:
- job: coverage-scheme:build
artifacts: true
script:
- 'echo "COVERAGE_SCHEME_IMAGE_REPOSITORY=$COVERAGE_SCHEME_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$COVERAGE_SCHEME_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$COVERAGE_SCHEME_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$COVERAGE_SCHEME_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$COVERAGE_SCHEME_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$COVERAGE_SCHEME_IMAGE_REPOSITORY:$COVERAGE_SCHEME_IMAGE_TAG" docker-context
- docker push "$COVERAGE_SCHEME_IMAGE_REPOSITORY:$COVERAGE_SCHEME_IMAGE_TAG"
- printf "COVERAGE_SCHEME_IMAGE=%s\n" "$COVERAGE_SCHEME_IMAGE_REPOSITORY:$COVERAGE_SCHEME_IMAGE_TAG" > coverage-scheme-image.env
artifacts:
reports:
dotenv: coverage-scheme-image.env
expire_in: 7 days
coverage-scheme:deploy:dev:
stage: deploy
extends:
- .coverage_scheme:rules:deploy-dev
- .coverage_scheme:helm-deploy
needs:
- job: coverage-scheme:package
artifacts: true
environment:
name: dev/pcp-coverage-scheme-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${COVERAGE_SCHEME_IMAGE%:*}"'
- 'export IMAGE_TAG="${COVERAGE_SCHEME_IMAGE##*:}"'
- >
helm upgrade --install "$COVERAGE_SCHEME_HELM_RELEASE"
"$COVERAGE_SCHEME_HELM_CHART_DIR"
--namespace "$COVERAGE_SCHEME_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$COVERAGE_SCHEME_HELM_CHART_DIR/values.yaml"
-f "$COVERAGE_SCHEME_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
coverage-scheme:deploy:master:
stage: deploy
extends:
- .coverage_scheme:rules:deploy-master
- .coverage_scheme:helm-deploy
needs:
- job: coverage-scheme:package
artifacts: true
environment:
name: master/pcp-coverage-scheme-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${COVERAGE_SCHEME_IMAGE%:*}"'
- 'export IMAGE_TAG="${COVERAGE_SCHEME_IMAGE##*:}"'
- >
helm upgrade --install "$COVERAGE_SCHEME_HELM_RELEASE"
"$COVERAGE_SCHEME_HELM_CHART_DIR"
--namespace "$COVERAGE_SCHEME_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$COVERAGE_SCHEME_HELM_CHART_DIR/values.yaml"
-f "$COVERAGE_SCHEME_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+247
View File
@@ -0,0 +1,247 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
DYNAMIC_PLAN_SERVICE_DIR: "services/pcp-dynamic-plan-service"
DYNAMIC_PLAN_HELM_CHART_DIR: "helm/pcp-dynamic-plan-service"
DYNAMIC_PLAN_HELM_RELEASE: "pcp-dynamic-plan-service"
DYNAMIC_PLAN_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-dynamic-plan-service"
DYNAMIC_PLAN_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
DYNAMIC_PLAN_K8S_NAMESPACE_DEV: "pcp-dev"
DYNAMIC_PLAN_K8S_NAMESPACE_MASTER: "pcp-master"
.dynamic_plan_changes: &dynamic_plan_changes
- services/pcp-dynamic-plan-service/**/*
- libs/ballistics-lib/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- gradle.properties
- .gitlab/ci/pcp-dynamic-plan-service.yml
- helm/pcp-dynamic-plan-service/**/*
- deploy/**/*
.dynamic_plan:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *dynamic_plan_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *dynamic_plan_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *dynamic_plan_changes
- when: never
.dynamic_plan:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *dynamic_plan_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *dynamic_plan_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *dynamic_plan_changes
- when: never
.dynamic_plan:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *dynamic_plan_changes
when: manual
- when: never
.dynamic_plan:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *dynamic_plan_changes
when: manual
- when: never
.dynamic_plan:gradle-cache:
cache:
key: "gradle-dynamic-plan-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.dynamic_plan:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.dynamic_plan:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
dynamic-plan:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .dynamic_plan:rules:test
- .dynamic_plan:gradle-cache
script:
- gradle --no-daemon :services:pcp-dynamic-plan-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-dynamic-plan-service/build/test-results/test/*.xml
paths:
- services/pcp-dynamic-plan-service/build/reports/tests/
expire_in: 7 days
dynamic-plan:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .dynamic_plan:rules:build-and-image
- .dynamic_plan:gradle-cache
needs:
- job: dynamic-plan:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-dynamic-plan-service:bootJar
artifacts:
when: always
paths:
- services/pcp-dynamic-plan-service/build/libs/*.jar
expire_in: 7 days
dynamic-plan:package:
stage: package
extends:
- .dynamic_plan:rules:build-and-image
- .dynamic_plan:docker-build
needs:
- job: dynamic-plan:build
artifacts: true
script:
- 'echo "DYNAMIC_PLAN_IMAGE_REPOSITORY=$DYNAMIC_PLAN_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$DYNAMIC_PLAN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$DYNAMIC_PLAN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$DYNAMIC_PLAN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$DYNAMIC_PLAN_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$DYNAMIC_PLAN_IMAGE_REPOSITORY:$DYNAMIC_PLAN_IMAGE_TAG" docker-context
- docker push "$DYNAMIC_PLAN_IMAGE_REPOSITORY:$DYNAMIC_PLAN_IMAGE_TAG"
- printf "DYNAMIC_PLAN_IMAGE=%s\n" "$DYNAMIC_PLAN_IMAGE_REPOSITORY:$DYNAMIC_PLAN_IMAGE_TAG" > dynamic-plan-image.env
artifacts:
reports:
dotenv: dynamic-plan-image.env
expire_in: 7 days
dynamic-plan:deploy:dev:
stage: deploy
extends:
- .dynamic_plan:rules:deploy-dev
- .dynamic_plan:helm-deploy
needs:
- job: dynamic-plan:package
artifacts: true
environment:
name: dev/pcp-dynamic-plan-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${DYNAMIC_PLAN_IMAGE%:*}"'
- 'export IMAGE_TAG="${DYNAMIC_PLAN_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$DYNAMIC_PLAN_HELM_RELEASE"
"$DYNAMIC_PLAN_HELM_CHART_DIR"
--namespace "$DYNAMIC_PLAN_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$DYNAMIC_PLAN_HELM_CHART_DIR/values.yaml"
-f "$DYNAMIC_PLAN_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
dynamic-plan:deploy:master:
stage: deploy
extends:
- .dynamic_plan:rules:deploy-master
- .dynamic_plan:helm-deploy
needs:
- job: dynamic-plan:package
artifacts: true
environment:
name: master/pcp-dynamic-plan-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${DYNAMIC_PLAN_IMAGE%:*}"'
- 'export IMAGE_TAG="${DYNAMIC_PLAN_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$DYNAMIC_PLAN_HELM_RELEASE"
"$DYNAMIC_PLAN_HELM_CHART_DIR"
--namespace "$DYNAMIC_PLAN_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$DYNAMIC_PLAN_HELM_CHART_DIR/values.yaml"
-f "$DYNAMIC_PLAN_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+191
View File
@@ -0,0 +1,191 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
GATEWAY_SERVICE_DIR: "services/pcp-gateway-service"
GATEWAY_HELM_CHART_DIR: "helm/pcp-gateway-service"
GATEWAY_HELM_RELEASE: "pcp-gateway-service"
GATEWAY_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-gateway-service"
GATEWAY_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
GATEWAY_K8S_NAMESPACE_DEV: "pcp-dev"
.gateway_changes: &gateway_changes
- services/pcp-gateway-service/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-gateway-service.yml
- helm/pcp-gateway-service/**/*
- deploy/**/*
.gateway:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *gateway_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *gateway_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *gateway_changes
- when: never
.gateway:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *gateway_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *gateway_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *gateway_changes
- when: never
.gateway:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *gateway_changes
when: manual
- when: never
.gateway:gradle-cache:
cache:
key: "gradle-gateway-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.gateway:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.gateway:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
gateway:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .gateway:rules:test
- .gateway:gradle-cache
script:
- gradle --no-daemon :services:pcp-gateway-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-gateway-service/build/test-results/test/*.xml
paths:
- services/pcp-gateway-service/build/reports/tests/
expire_in: 7 days
gateway:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .gateway:rules:build-and-image
- .gateway:gradle-cache
needs:
- job: gateway:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-gateway-service:bootJar
artifacts:
when: always
paths:
- services/pcp-gateway-service/build/libs/*.jar
expire_in: 7 days
gateway:package:
stage: package
extends:
- .gateway:rules:build-and-image
- .gateway:docker-build
needs:
- job: gateway:build
artifacts: true
script:
- 'echo "GATEWAY_IMAGE_REPOSITORY=$GATEWAY_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$GATEWAY_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$GATEWAY_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$GATEWAY_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$GATEWAY_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$GATEWAY_IMAGE_REPOSITORY:$GATEWAY_IMAGE_TAG" docker-context
- docker push "$GATEWAY_IMAGE_REPOSITORY:$GATEWAY_IMAGE_TAG"
- printf "GATEWAY_IMAGE=%s\n" "$GATEWAY_IMAGE_REPOSITORY:$GATEWAY_IMAGE_TAG" > gateway-image.env
artifacts:
reports:
dotenv: gateway-image.env
expire_in: 7 days
gateway:deploy:dev:
stage: deploy
extends:
- .gateway:rules:deploy-dev
- .gateway:helm-deploy
needs:
- job: gateway:package
artifacts: true
environment:
name: dev/pcp-gateway-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${GATEWAY_IMAGE%:*}"'
- 'export IMAGE_TAG="${GATEWAY_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$GATEWAY_HELM_RELEASE"
"$GATEWAY_HELM_CHART_DIR"
--namespace "$GATEWAY_K8S_NAMESPACE_DEV"
--create-namespace
--values "$GATEWAY_HELM_CHART_DIR/values.yaml"
--values "$GATEWAY_HELM_CHART_DIR/values-dev.yaml"
--set-string image.repository="$IMAGE_REPOSITORY"
--set-string image.tag="$IMAGE_TAG"
--set-string app.profile=dev
--set-string app.configLabel=dev
--atomic
--wait
--timeout 15m
+246
View File
@@ -0,0 +1,246 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
MISSION_PLANING_SERVICE_DIR: "services/pcp-mission-planing-service"
MISSION_PLANING_HELM_CHART_DIR: "helm/pcp-mission-planing-service"
MISSION_PLANING_HELM_RELEASE: "pcp-mission-planing-service"
MISSION_PLANING_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-mission-planing-service"
MISSION_PLANING_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
MISSION_PLANING_K8S_NAMESPACE_DEV: "pcp-dev"
MISSION_PLANING_K8S_NAMESPACE_MASTER: "pcp-master"
.mission_planing_changes: &mission_planing_changes
- services/pcp-mission-planing-service/**/*
- libs/ballistics-lib/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-mission-planing-service.yml
- helm/pcp-mission-planing-service/**/*
- deploy/**/*
.mission-planing:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *mission_planing_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *mission_planing_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *mission_planing_changes
- when: never
.mission-planing:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *mission_planing_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *mission_planing_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *mission_planing_changes
- when: never
.mission-planing:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *mission_planing_changes
when: manual
- when: never
.mission-planing:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *mission_planing_changes
when: manual
- when: never
.mission-planing:gradle-cache:
cache:
key: "gradle-mission-planing-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.mission-planing:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.mission-planing:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
mission-planing:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .mission-planing:rules:test
- .mission-planing:gradle-cache
script:
- gradle --no-daemon :services:pcp-mission-planing-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-mission-planing-service/build/test-results/test/*.xml
paths:
- services/pcp-mission-planing-service/build/reports/tests/
expire_in: 7 days
mission-planing:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .mission-planing:rules:build-and-image
- .mission-planing:gradle-cache
needs:
- job: mission-planing:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-mission-planing-service:bootJar
artifacts:
when: always
paths:
- services/pcp-mission-planing-service/build/libs/*.jar
expire_in: 7 days
mission-planing:package:
stage: package
extends:
- .mission-planing:rules:build-and-image
- .mission-planing:docker-build
needs:
- job: mission-planing:build
artifacts: true
script:
- 'echo "MISSION_PLANING_IMAGE_REPOSITORY=$MISSION_PLANING_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$MISSION_PLANING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$MISSION_PLANING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$MISSION_PLANING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$MISSION_PLANING_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$MISSION_PLANING_IMAGE_REPOSITORY:$MISSION_PLANING_IMAGE_TAG" docker-context
- docker push "$MISSION_PLANING_IMAGE_REPOSITORY:$MISSION_PLANING_IMAGE_TAG"
- printf "MISSION_PLANING_IMAGE=%s\n" "$MISSION_PLANING_IMAGE_REPOSITORY:$MISSION_PLANING_IMAGE_TAG" > mission-planing-image.env
artifacts:
reports:
dotenv: mission-planing-image.env
expire_in: 7 days
mission-planing:deploy:dev:
stage: deploy
extends:
- .mission-planing:rules:deploy-dev
- .mission-planing:helm-deploy
needs:
- job: mission-planing:package
artifacts: true
environment:
name: dev/pcp-mission-planing-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${MISSION_PLANING_IMAGE%:*}"'
- 'export IMAGE_TAG="${MISSION_PLANING_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$MISSION_PLANING_HELM_RELEASE"
"$MISSION_PLANING_HELM_CHART_DIR"
--namespace "$MISSION_PLANING_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$MISSION_PLANING_HELM_CHART_DIR/values.yaml"
-f "$MISSION_PLANING_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
mission-planing:deploy:master:
stage: deploy
extends:
- .mission-planing:rules:deploy-master
- .mission-planing:helm-deploy
needs:
- job: mission-planing:package
artifacts: true
environment:
name: master/pcp-mission-planing-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${MISSION_PLANING_IMAGE%:*}"'
- 'export IMAGE_TAG="${MISSION_PLANING_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$MISSION_PLANING_HELM_RELEASE"
"$MISSION_PLANING_HELM_CHART_DIR"
--namespace "$MISSION_PLANING_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$MISSION_PLANING_HELM_CHART_DIR/values.yaml"
-f "$MISSION_PLANING_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+247
View File
@@ -0,0 +1,247 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
REQUEST_SERVICE_DIR: "services/pcp-request-service"
REQUEST_HELM_CHART_DIR: "helm/pcp-request-service"
REQUEST_HELM_RELEASE: "pcp-request-service"
REQUEST_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-request-service"
REQUEST_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
REQUEST_K8S_NAMESPACE_DEV: "pcp-dev"
REQUEST_K8S_NAMESPACE_MASTER: "pcp-master"
.request_changes: &request_changes
- services/pcp-request-service/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- gradle.properties
- gradle/**/*
- .gitlab/ci/pcp-request-service.yml
- helm/pcp-request-service/**/*
- deploy/**/*
.request:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *request_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *request_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *request_changes
- when: never
.request:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *request_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *request_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *request_changes
- when: never
.request:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *request_changes
when: manual
- when: never
.request:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *request_changes
when: manual
- when: never
.request:gradle-cache:
cache:
key: "gradle-request-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.request:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.request:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
request:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .request:rules:test
- .request:gradle-cache
script:
- gradle --no-daemon :services:pcp-request-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-request-service/build/test-results/test/*.xml
paths:
- services/pcp-request-service/build/reports/tests/
expire_in: 7 days
request:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .request:rules:build-and-image
- .request:gradle-cache
needs:
- job: request:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-request-service:bootJar
artifacts:
when: always
paths:
- services/pcp-request-service/build/libs/*.jar
expire_in: 7 days
request:package:
stage: package
extends:
- .request:rules:build-and-image
- .request:docker-build
needs:
- job: request:build
artifacts: true
script:
- 'echo "REQUEST_IMAGE_REPOSITORY=$REQUEST_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$REQUEST_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$REQUEST_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$REQUEST_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$REQUEST_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$REQUEST_IMAGE_REPOSITORY:$REQUEST_IMAGE_TAG" docker-context
- docker push "$REQUEST_IMAGE_REPOSITORY:$REQUEST_IMAGE_TAG"
- printf "REQUEST_IMAGE=%s\n" "$REQUEST_IMAGE_REPOSITORY:$REQUEST_IMAGE_TAG" > request-image.env
artifacts:
reports:
dotenv: request-image.env
expire_in: 7 days
request:deploy:dev:
stage: deploy
extends:
- .request:rules:deploy-dev
- .request:helm-deploy
needs:
- job: request:package
artifacts: true
environment:
name: dev/pcp-request-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${REQUEST_IMAGE%:*}"'
- 'export IMAGE_TAG="${REQUEST_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$REQUEST_HELM_RELEASE"
"$REQUEST_HELM_CHART_DIR"
--namespace "$REQUEST_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 15m
-f "$REQUEST_HELM_CHART_DIR/values.yaml"
-f "$REQUEST_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
request:deploy:master:
stage: deploy
extends:
- .request:rules:deploy-master
- .request:helm-deploy
needs:
- job: request:package
artifacts: true
environment:
name: master/pcp-request-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${REQUEST_IMAGE%:*}"'
- 'export IMAGE_TAG="${REQUEST_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$REQUEST_HELM_RELEASE"
"$REQUEST_HELM_CHART_DIR"
--namespace "$REQUEST_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 15m
-f "$REQUEST_HELM_CHART_DIR/values.yaml"
-f "$REQUEST_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
@@ -0,0 +1,245 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
SATELLITE_CATALOG_SERVICE_DIR: "services/pcp-satellite-catalog-service"
SATELLITE_CATALOG_HELM_CHART_DIR: "helm/pcp-satellite-catalog-service"
SATELLITE_CATALOG_HELM_RELEASE: "pcp-satellite-catalog-service"
SATELLITE_CATALOG_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-satellite-catalog-service"
SATELLITE_CATALOG_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
SATELLITE_CATALOG_K8S_NAMESPACE_DEV: "pcp-dev"
SATELLITE_CATALOG_K8S_NAMESPACE_MASTER: "pcp-master"
.satellite_catalog_changes: &satellite_catalog_changes
- services/pcp-satellite-catalog-service/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-satellite-catalog-service.*
- helm/pcp-satellite-catalog-service/**/*
- deploy/**/*
.satellite-catalog:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *satellite_catalog_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *satellite_catalog_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *satellite_catalog_changes
- when: never
.satellite-catalog:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *satellite_catalog_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *satellite_catalog_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *satellite_catalog_changes
- when: never
.satellite-catalog:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *satellite_catalog_changes
when: manual
- when: never
.satellite-catalog:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *satellite_catalog_changes
when: manual
- when: never
.satellite-catalog:gradle-cache:
cache:
key: "gradle-satellite-catalog-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.satellite-catalog:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.satellite-catalog:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
satellite-catalog:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .satellite-catalog:rules:test
- .satellite-catalog:gradle-cache
script:
- gradle --no-daemon :services:pcp-satellite-catalog-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-satellite-catalog-service/build/test-results/test/*.xml
paths:
- services/pcp-satellite-catalog-service/build/reports/tests/
expire_in: 7 days
satellite-catalog:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .satellite-catalog:rules:build-and-image
- .satellite-catalog:gradle-cache
needs:
- job: satellite-catalog:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-satellite-catalog-service:bootJar
artifacts:
when: always
paths:
- services/pcp-satellite-catalog-service/build/libs/*.jar
expire_in: 7 days
satellite-catalog:package:
stage: package
extends:
- .satellite-catalog:rules:build-and-image
- .satellite-catalog:docker-build
needs:
- job: satellite-catalog:build
artifacts: true
script:
- 'echo "SATELLITE_CATALOG_IMAGE_REPOSITORY=$SATELLITE_CATALOG_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$SATELLITE_CATALOG_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$SATELLITE_CATALOG_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$SATELLITE_CATALOG_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$SATELLITE_CATALOG_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$SATELLITE_CATALOG_IMAGE_REPOSITORY:$SATELLITE_CATALOG_IMAGE_TAG" docker-context
- docker push "$SATELLITE_CATALOG_IMAGE_REPOSITORY:$SATELLITE_CATALOG_IMAGE_TAG"
- printf "SATELLITE_CATALOG_IMAGE=%s\n" "$SATELLITE_CATALOG_IMAGE_REPOSITORY:$SATELLITE_CATALOG_IMAGE_TAG" > satellite-catalog-image.env
artifacts:
reports:
dotenv: satellite-catalog-image.env
expire_in: 7 days
satellite-catalog:deploy:dev:
stage: deploy
extends:
- .satellite-catalog:rules:deploy-dev
- .satellite-catalog:helm-deploy
needs:
- job: satellite-catalog:package
artifacts: true
environment:
name: dev/pcp-satellite-catalog-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${SATELLITE_CATALOG_IMAGE%:*}"'
- 'export IMAGE_TAG="${SATELLITE_CATALOG_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$SATELLITE_CATALOG_HELM_RELEASE"
"$SATELLITE_CATALOG_HELM_CHART_DIR"
--namespace "$SATELLITE_CATALOG_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$SATELLITE_CATALOG_HELM_CHART_DIR/values.yaml"
-f "$SATELLITE_CATALOG_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
satellite-catalog:deploy:master:
stage: deploy
extends:
- .satellite-catalog:rules:deploy-master
- .satellite-catalog:helm-deploy
needs:
- job: satellite-catalog:package
artifacts: true
environment:
name: master/pcp-satellite-catalog-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${SATELLITE_CATALOG_IMAGE%:*}"'
- 'export IMAGE_TAG="${SATELLITE_CATALOG_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$SATELLITE_CATALOG_HELM_RELEASE"
"$SATELLITE_CATALOG_HELM_CHART_DIR"
--namespace "$SATELLITE_CATALOG_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$SATELLITE_CATALOG_HELM_CHART_DIR/values.yaml"
-f "$SATELLITE_CATALOG_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+191
View File
@@ -0,0 +1,191 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
SPRING_BOOT_ADMIN_SERVICE_DIR: "services/pcp-srpring-boot-admin-server"
SPRING_BOOT_ADMIN_HELM_CHART_DIR: "helm/pcp-spring-boot-admin-server"
SPRING_BOOT_ADMIN_HELM_RELEASE: "pcp-spring-boot-admin-server"
SPRING_BOOT_ADMIN_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-spring-boot-admin-server"
SPRING_BOOT_ADMIN_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
SPRING_BOOT_ADMIN_K8S_NAMESPACE_DEV: "pcp-dev"
.spring_boot_admin_changes: &spring_boot_admin_changes
- services/pcp-srpring-boot-admin-server/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-spring-boot-admin-server.yml
- helm/pcp-spring-boot-admin-server/**/*
- deploy/**/*
.spring-boot-admin:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *spring_boot_admin_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *spring_boot_admin_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *spring_boot_admin_changes
- when: never
.spring-boot-admin:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *spring_boot_admin_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *spring_boot_admin_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *spring_boot_admin_changes
- when: never
.spring-boot-admin:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *spring_boot_admin_changes
when: manual
- when: never
.spring-boot-admin:gradle-cache:
cache:
key: "gradle-spring-boot-admin-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.spring-boot-admin:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.spring-boot-admin:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
spring-boot-admin:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .spring-boot-admin:rules:test
- .spring-boot-admin:gradle-cache
script:
- gradle --no-daemon :services:pcp-srpring-boot-admin-server:test
artifacts:
when: always
reports:
junit:
- services/pcp-srpring-boot-admin-server/build/test-results/test/*.xml
paths:
- services/pcp-srpring-boot-admin-server/build/reports/tests/
expire_in: 7 days
spring-boot-admin:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .spring-boot-admin:rules:build-and-image
- .spring-boot-admin:gradle-cache
needs:
- job: spring-boot-admin:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-srpring-boot-admin-server:bootJar
artifacts:
when: always
paths:
- services/pcp-srpring-boot-admin-server/build/libs/*.jar
expire_in: 7 days
spring-boot-admin:package:
stage: package
extends:
- .spring-boot-admin:rules:build-and-image
- .spring-boot-admin:docker-build
needs:
- job: spring-boot-admin:build
artifacts: true
script:
- 'echo "SPRING_BOOT_ADMIN_IMAGE_REPOSITORY=$SPRING_BOOT_ADMIN_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$SPRING_BOOT_ADMIN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$SPRING_BOOT_ADMIN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$SPRING_BOOT_ADMIN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$SPRING_BOOT_ADMIN_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$SPRING_BOOT_ADMIN_IMAGE_REPOSITORY:$SPRING_BOOT_ADMIN_IMAGE_TAG" docker-context
- docker push "$SPRING_BOOT_ADMIN_IMAGE_REPOSITORY:$SPRING_BOOT_ADMIN_IMAGE_TAG"
- printf "SPRING_BOOT_ADMIN_IMAGE=%s\n" "$SPRING_BOOT_ADMIN_IMAGE_REPOSITORY:$SPRING_BOOT_ADMIN_IMAGE_TAG" > spring-boot-admin-image.env
artifacts:
reports:
dotenv: spring-boot-admin-image.env
expire_in: 7 days
spring-boot-admin:deploy:dev:
stage: deploy
extends:
- .spring-boot-admin:rules:deploy-dev
- .spring-boot-admin:helm-deploy
needs:
- job: spring-boot-admin:package
artifacts: true
environment:
name: dev/pcp-spring-boot-admin-server
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${SPRING_BOOT_ADMIN_IMAGE%:*}"'
- 'export IMAGE_TAG="${SPRING_BOOT_ADMIN_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$SPRING_BOOT_ADMIN_HELM_RELEASE"
"$SPRING_BOOT_ADMIN_HELM_CHART_DIR"
--namespace "$SPRING_BOOT_ADMIN_K8S_NAMESPACE_DEV"
--create-namespace
--values "$SPRING_BOOT_ADMIN_HELM_CHART_DIR/values.yaml"
--values "$SPRING_BOOT_ADMIN_HELM_CHART_DIR/values-dev.yaml"
--set-string image.repository="$IMAGE_REPOSITORY"
--set-string image.tag="$IMAGE_TAG"
--set-string app.profile=dev
--set-string app.configLabel=dev
--atomic
--wait
--timeout 15m
+245
View File
@@ -0,0 +1,245 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
STATIONS_SERVICE_DIR: "services/pcp-stations-service"
STATIONS_HELM_CHART_DIR: "helm/pcp-stations-service"
STATIONS_HELM_RELEASE: "pcp-stations-service"
STATIONS_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-stations-service"
STATIONS_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
STATIONS_K8S_NAMESPACE_DEV: "pcp-dev"
STATIONS_K8S_NAMESPACE_MASTER: "pcp-master"
.stations_changes: &stations_changes
- services/pcp-stations-service/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-stations-service.*
- helm/pcp-stations-service/**/*
- deploy/**/*
.stations:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *stations_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *stations_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *stations_changes
- when: never
.stations:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *stations_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *stations_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *stations_changes
- when: never
.stations:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *stations_changes
when: manual
- when: never
.stations:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *stations_changes
when: manual
- when: never
.stations:gradle-cache:
cache:
key: "gradle-stations-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.stations:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.stations:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
stations:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .stations:rules:test
- .stations:gradle-cache
script:
- gradle --no-daemon :services:pcp-stations-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-stations-service/build/test-results/test/*.xml
paths:
- services/pcp-stations-service/build/reports/tests/
expire_in: 7 days
stations:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .stations:rules:build-and-image
- .stations:gradle-cache
needs:
- job: stations:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-stations-service:bootJar
artifacts:
when: always
paths:
- services/pcp-stations-service/build/libs/*.jar
expire_in: 7 days
stations:package:
stage: package
extends:
- .stations:rules:build-and-image
- .stations:docker-build
needs:
- job: stations:build
artifacts: true
script:
- 'echo "STATIONS_IMAGE_REPOSITORY=$STATIONS_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$STATIONS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$STATIONS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$STATIONS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$STATIONS_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$STATIONS_IMAGE_REPOSITORY:$STATIONS_IMAGE_TAG" docker-context
- docker push "$STATIONS_IMAGE_REPOSITORY:$STATIONS_IMAGE_TAG"
- printf "STATIONS_IMAGE=%s\n" "$STATIONS_IMAGE_REPOSITORY:$STATIONS_IMAGE_TAG" > stations-image.env
artifacts:
reports:
dotenv: stations-image.env
expire_in: 7 days
stations:deploy:dev:
stage: deploy
extends:
- .stations:rules:deploy-dev
- .stations:helm-deploy
needs:
- job: stations:package
artifacts: true
environment:
name: dev/pcp-stations-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${STATIONS_IMAGE%:*}"'
- 'export IMAGE_TAG="${STATIONS_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$STATIONS_HELM_RELEASE"
"$STATIONS_HELM_CHART_DIR"
--namespace "$STATIONS_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$STATIONS_HELM_CHART_DIR/values.yaml"
-f "$STATIONS_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
stations:deploy:master:
stage: deploy
extends:
- .stations:rules:deploy-master
- .stations:helm-deploy
needs:
- job: stations:package
artifacts: true
environment:
name: master/pcp-stations-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${STATIONS_IMAGE%:*}"'
- 'export IMAGE_TAG="${STATIONS_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$STATIONS_HELM_RELEASE"
"$STATIONS_HELM_CHART_DIR"
--namespace "$STATIONS_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$STATIONS_HELM_CHART_DIR/values.yaml"
-f "$STATIONS_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+203
View File
@@ -0,0 +1,203 @@
variables:
TGU_SERVICE_DIR: "services/pcp-tgu-service"
TGU_HELM_CHART_DIR: "helm/pcp-tgu-service"
TGU_HELM_RELEASE: "pcp-tgu-service"
TGU_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-tgu-service"
TGU_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
TGU_K8S_NAMESPACE_DEV: "pcp-dev"
TGU_K8S_NAMESPACE_MASTER: "pcp-master"
.tgu_changes: &tgu_changes
- services/pcp-tgu-service/**/*
# - libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-tgu-service.yml
- helm/pcp-tgu-service/**/*
- deploy/**/*
.tgu:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *tgu_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *tgu_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *tgu_changes
- when: never
.tgu:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *tgu_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *tgu_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *tgu_changes
- when: never
.tgu:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *tgu_changes
when: manual
- when: never
.tgu:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *tgu_changes
when: manual
- when: never
.tgu:gradle-cache:
cache:
key: "gradle-tgu-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.tgu:docker-build:
image: docker:26
services:
- name: docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.tgu:helm-deploy:
image:
name: alpine/helm:3.16.4
entrypoint: [""]
before_script:
- mkdir -p ~/.kube
- helm version
tgu:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .tgu:rules:test
- .tgu:gradle-cache
script:
- gradle --no-daemon :services:pcp-tgu-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-tgu-service/build/test-results/test/*.xml
paths:
- services/pcp-tgu-service/build/reports/tests/
expire_in: 7 days
tgu:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .tgu:rules:build-and-image
- .tgu:gradle-cache
needs:
- job: tgu:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-tgu-service:bootJar
artifacts:
when: always
paths:
- services/pcp-tgu-service/build/libs/*.jar
expire_in: 7 days
tgu:package:
stage: package
extends:
- .tgu:rules:build-and-image
- .tgu:docker-build
needs:
- job: tgu:build
artifacts: true
script:
- 'echo "TGU_IMAGE_REPOSITORY=$TGU_IMAGE_REPOSITORY"'
- mkdir -p docker-context/build/libs
- cp "$TGU_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$TGU_SERVICE_DIR"/build/libs/*.jar docker-context/build/libs/
- docker build --file docker-context/Dockerfile --tag "$TGU_IMAGE_REPOSITORY:$TGU_IMAGE_TAG" docker-context
- docker push "$TGU_IMAGE_REPOSITORY:$TGU_IMAGE_TAG"
- printf "TGU_IMAGE=%s\n" "$TGU_IMAGE_REPOSITORY:$TGU_IMAGE_TAG" > tgu-image.env
artifacts:
reports:
dotenv: tgu-image.env
expire_in: 7 days
tgu:deploy:dev:
stage: deploy
extends:
- .tgu:rules:deploy-dev
- .tgu:helm-deploy
needs:
- job: tgu:package
artifacts: true
environment:
name: dev/pcp-tgu-service
script:
- 'echo "$KUBE_CONFIG_DEV_B64" | base64 -d > ~/.kube/config'
- chmod 600 ~/.kube/config
- 'export IMAGE_REPOSITORY="${TGU_IMAGE%:*}"'
- 'export IMAGE_TAG="${TGU_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$TGU_HELM_RELEASE"
"$TGU_HELM_CHART_DIR"
--namespace "$TGU_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$TGU_HELM_CHART_DIR/values.yaml"
-f "$TGU_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
tgu:deploy:master:
stage: deploy
extends:
- .tgu:rules:deploy-master
- .tgu:helm-deploy
needs:
- job: tgu:package
artifacts: true
environment:
name: master/pcp-tgu-service
script:
- 'echo "$KUBE_CONFIG_MASTER_B64" | base64 -d > ~/.kube/config'
- chmod 600 ~/.kube/config
- 'export IMAGE_REPOSITORY="${TGU_IMAGE%:*}"'
- 'export IMAGE_TAG="${TGU_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$TGU_HELM_RELEASE"
"$TGU_HELM_CHART_DIR"
--namespace "$TGU_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$TGU_HELM_CHART_DIR/values.yaml"
-f "$TGU_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+187
View File
@@ -0,0 +1,187 @@
# pcp-tgu-ui-service — статический фронт ТГУ (Node/Vite → nginx). В отличие от
# остальных сервисов группы (gradle/Spring), собирается на Node: test = vitest,
# build = npm run build (tsc+vite), package = docker по своему Dockerfile.
variables:
TGU_UI_SERVICE_DIR: "services/pcp-tgu-ui-service"
TGU_UI_HELM_CHART_DIR: "helm/pcp-tgu-ui-service"
TGU_UI_HELM_RELEASE: "pcp-tgu-ui-service"
TGU_UI_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-tgu-ui-service"
TGU_UI_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
TGU_UI_K8S_NAMESPACE_DEV: "pcp-dev"
TGU_UI_K8S_NAMESPACE_MASTER: "pcp-master"
TGU_UI_NODE_IMAGE: "repo.nstart.cloud/nstart/node:24-trixie-slim"
.tgu_ui_changes: &tgu_ui_changes
- services/pcp-tgu-ui-service/**/*
- .gitlab/ci/pcp-tgu-ui-service.yml
- helm/pcp-tgu-ui-service/**/*
- deploy/**/*
.tgu-ui:rules:build-and-image:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *tgu_ui_changes
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
changes: *tgu_ui_changes
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"'
changes: *tgu_ui_changes
- when: never
.tgu-ui:rules:test:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *tgu_ui_changes
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
changes: *tgu_ui_changes
when: manual
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"'
changes: *tgu_ui_changes
- when: never
.tgu-ui:rules:deploy-dev:
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
changes: *tgu_ui_changes
when: manual
- when: never
.tgu-ui:rules:deploy-master:
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"'
changes: *tgu_ui_changes
when: manual
- when: never
.tgu-ui:npm-cache:
cache:
key: "npm-tgu-ui-$CI_DEFAULT_BRANCH"
paths:
- services/pcp-tgu-ui-service/.npm
policy: pull-push
.tgu-ui:docker-build:
image: docker:26
services:
- name: docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.tgu-ui:helm-deploy:
image:
name: alpine/helm:3.16.4
entrypoint: [""]
before_script:
- mkdir -p ~/.kube
- helm version
tgu-ui:test:
stage: test
image: $TGU_UI_NODE_IMAGE
extends:
- .tgu-ui:rules:test
- .tgu-ui:npm-cache
script:
- cd "$TGU_UI_SERVICE_DIR"
- npm ci --cache .npm --prefer-offline
- npm run test
tgu-ui:build:
stage: build
image: $TGU_UI_NODE_IMAGE
extends:
- .tgu-ui:rules:build-and-image
- .tgu-ui:npm-cache
script:
- cd "$TGU_UI_SERVICE_DIR"
- npm ci --cache .npm --prefer-offline
- npm run build
artifacts:
when: always
paths:
- services/pcp-tgu-ui-service/dist/
expire_in: 7 days
# Образ собирает сам Dockerfile (multi-stage: npm ci + build внутри). Артефакт
# tgu-ui:build — быстрый gate на tsc+vite перед более тяжёлым docker build.
tgu-ui:package:
stage: package
extends:
- .tgu-ui:rules:build-and-image
- .tgu-ui:docker-build
needs:
- job: tgu-ui:build
artifacts: false
script:
- 'echo "TGU_UI_IMAGE_REPOSITORY=$TGU_UI_IMAGE_REPOSITORY"'
- docker build --file "$TGU_UI_SERVICE_DIR/Dockerfile" --tag "$TGU_UI_IMAGE_REPOSITORY:$TGU_UI_IMAGE_TAG" "$TGU_UI_SERVICE_DIR"
- docker push "$TGU_UI_IMAGE_REPOSITORY:$TGU_UI_IMAGE_TAG"
- printf "TGU_UI_IMAGE=%s\n" "$TGU_UI_IMAGE_REPOSITORY:$TGU_UI_IMAGE_TAG" > tgu-ui-image.env
artifacts:
reports:
dotenv: tgu-ui-image.env
expire_in: 7 days
tgu-ui:deploy:dev:
stage: deploy
extends:
- .tgu-ui:rules:deploy-dev
- .tgu-ui:helm-deploy
needs:
- job: tgu-ui:package
artifacts: true
environment:
name: dev/pcp-tgu-ui-service
script:
- 'echo "$KUBE_CONFIG_DEV_B64" | base64 -d > ~/.kube/config'
- chmod 600 ~/.kube/config
- 'export IMAGE_REPOSITORY="${TGU_UI_IMAGE%:*}"'
- 'export IMAGE_TAG="${TGU_UI_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$TGU_UI_HELM_RELEASE"
"$TGU_UI_HELM_CHART_DIR"
--namespace "$TGU_UI_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$TGU_UI_HELM_CHART_DIR/values.yaml"
-f "$TGU_UI_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
tgu-ui:deploy:master:
stage: deploy
extends:
- .tgu-ui:rules:deploy-master
- .tgu-ui:helm-deploy
needs:
- job: tgu-ui:package
artifacts: true
environment:
name: master/pcp-tgu-ui-service
script:
- 'echo "$KUBE_CONFIG_MASTER_B64" | base64 -d > ~/.kube/config'
- chmod 600 ~/.kube/config
- 'export IMAGE_REPOSITORY="${TGU_UI_IMAGE%:*}"'
- 'export IMAGE_TAG="${TGU_UI_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$TGU_UI_HELM_RELEASE"
"$TGU_UI_HELM_CHART_DIR"
--namespace "$TGU_UI_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$TGU_UI_HELM_CHART_DIR/values.yaml"
-f "$TGU_UI_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
+246
View File
@@ -0,0 +1,246 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
UI_SERVICE_DIR: "services/pcp-ui-service"
UI_HELM_CHART_DIR: "helm/pcp-ui-service"
UI_HELM_RELEASE: "pcp-ui-service"
UI_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-ui-service"
UI_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
UI_K8S_NAMESPACE_DEV: "pcp-dev"
UI_K8S_NAMESPACE_MASTER: "pcp-master"
.ui_changes: &ui_changes
- services/pcp-ui-service/**/*
- libs/ballistics-lib/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/pcp-ui-service.yml
- helm/pcp-ui-service/**/*
- deploy/**/*
.ui:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *ui_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *ui_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *ui_changes
- when: never
.ui:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *ui_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *ui_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *ui_changes
- when: never
.ui:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *ui_changes
when: manual
- when: never
.ui:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *ui_changes
when: manual
- when: never
.ui:gradle-cache:
cache:
key: "gradle-ui-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.ui:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.ui:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
ui:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .ui:rules:test
- .ui:gradle-cache
script:
- gradle --no-daemon :services:pcp-ui-service:test
artifacts:
when: always
reports:
junit:
- services/pcp-ui-service/build/test-results/test/*.xml
paths:
- services/pcp-ui-service/build/reports/tests/
expire_in: 7 days
ui:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .ui:rules:build-and-image
- .ui:gradle-cache
needs:
- job: ui:test
artifacts: true
script:
- gradle --no-daemon :services:pcp-ui-service:bootJar
artifacts:
when: always
paths:
- services/pcp-ui-service/build/libs/*.jar
expire_in: 7 days
ui:package:
stage: package
extends:
- .ui:rules:build-and-image
- .ui:docker-build
needs:
- job: ui:build
artifacts: true
script:
- 'echo "UI_IMAGE_REPOSITORY=$UI_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$UI_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$UI_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$UI_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$UI_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$UI_IMAGE_REPOSITORY:$UI_IMAGE_TAG" docker-context
- docker push "$UI_IMAGE_REPOSITORY:$UI_IMAGE_TAG"
- printf "UI_IMAGE=%s\n" "$UI_IMAGE_REPOSITORY:$UI_IMAGE_TAG" > ui-image.env
artifacts:
reports:
dotenv: ui-image.env
expire_in: 7 days
ui:deploy:dev:
stage: deploy
extends:
- .ui:rules:deploy-dev
- .ui:helm-deploy
needs:
- job: ui:package
artifacts: true
environment:
name: dev/pcp-ui-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${UI_IMAGE%:*}"'
- 'export IMAGE_TAG="${UI_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$UI_HELM_RELEASE"
"$UI_HELM_CHART_DIR"
--namespace "$UI_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$UI_HELM_CHART_DIR/values.yaml"
-f "$UI_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
ui:deploy:master:
stage: deploy
extends:
- .ui:rules:deploy-master
- .ui:helm-deploy
needs:
- job: ui:package
artifacts: true
environment:
name: master/pcp-ui-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${UI_IMAGE%:*}"'
- 'export IMAGE_TAG="${UI_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$UI_HELM_RELEASE"
"$UI_HELM_CHART_DIR"
--namespace "$UI_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 10m
-f "$UI_HELM_CHART_DIR/values.yaml"
-f "$UI_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+256
View File
@@ -0,0 +1,256 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
SLOTS_SERVICE_DIR: "services/slots-service"
SLOTS_HELM_CHART_DIR: "helm/slots-service"
SLOTS_HELM_RELEASE: "slots-service"
SLOTS_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/slots-service"
SLOTS_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
SLOTS_K8S_NAMESPACE_DEV: "pcp-dev"
SLOTS_K8S_NAMESPACE_MASTER: "pcp-master"
.slots_changes: &slots_changes
- services/slots-service/**/*
- libs/ballistics-lib/**/*
- libs/pcp-types-lib/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/slots-service.yml
- helm/slots-service/**/*
- deploy/**/*
.slots:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *slots_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *slots_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *slots_changes
- when: never
.slots:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *slots_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *slots_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *slots_changes
- when: never
.slots:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *slots_changes
when: manual
- when: never
.slots:rules:deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *slots_changes
when: manual
- when: never
.slots:gradle-cache:
cache:
key: "gradle-slots-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.slots:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.slots:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
slots:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
services:
- name: repo.nstart.cloud/postgis/postgis:16-3.4
alias: postgres
variables:
POSTGRES_DB: "pcp_slots"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "password"
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/pcp_slots"
SPRING_DATASOURCE_USERNAME: "postgres"
SPRING_DATASOURCE_PASSWORD: "password"
extends:
- .slots:rules:test
- .slots:gradle-cache
script:
- gradle --no-daemon :services:slots-service:test
artifacts:
when: always
reports:
junit:
- services/slots-service/build/test-results/test/*.xml
paths:
- services/slots-service/build/reports/tests/
expire_in: 7 days
slots:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .slots:rules:build-and-image
- .slots:gradle-cache
needs:
- job: slots:test
artifacts: true
script:
- gradle --no-daemon :services:slots-service:bootJar
artifacts:
when: always
paths:
- services/slots-service/build/libs/*.jar
expire_in: 7 days
slots:package:
stage: package
extends:
- .slots:rules:build-and-image
- .slots:docker-build
needs:
- job: slots:build
artifacts: true
script:
- 'echo "SLOTS_IMAGE_REPOSITORY=$SLOTS_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$SLOTS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$SLOTS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$SLOTS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$SLOTS_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$SLOTS_IMAGE_REPOSITORY:$SLOTS_IMAGE_TAG" docker-context
- docker push "$SLOTS_IMAGE_REPOSITORY:$SLOTS_IMAGE_TAG"
- printf "SLOTS_IMAGE=%s\n" "$SLOTS_IMAGE_REPOSITORY:$SLOTS_IMAGE_TAG" > slots-image.env
artifacts:
reports:
dotenv: slots-image.env
expire_in: 7 days
slots:deploy:dev:
stage: deploy
extends:
- .slots:rules:deploy-dev
- .slots:helm-deploy
needs:
- job: slots:package
artifacts: true
environment:
name: dev/pcp-slots-service
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${SLOTS_IMAGE%:*}"'
- 'export IMAGE_TAG="${SLOTS_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$SLOTS_HELM_RELEASE"
"$SLOTS_HELM_CHART_DIR"
--namespace "$SLOTS_K8S_NAMESPACE_DEV"
--create-namespace
--wait
--atomic
--timeout 30m
-f "$SLOTS_HELM_CHART_DIR/values.yaml"
-f "$SLOTS_HELM_CHART_DIR/values-dev.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=dev
--set app.configLabel=dev
slots:deploy:master:
stage: deploy
extends:
- .slots:rules:deploy-master
- .slots:helm-deploy
needs:
- job: slots:package
artifacts: true
environment:
name: master/pcp-slots-service
script:
- |
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- 'export IMAGE_REPOSITORY="${SLOTS_IMAGE%:*}"'
- 'export IMAGE_TAG="${SLOTS_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$SLOTS_HELM_RELEASE"
"$SLOTS_HELM_CHART_DIR"
--namespace "$SLOTS_K8S_NAMESPACE_MASTER"
--create-namespace
--wait
--atomic
--timeout 30m
-f "$SLOTS_HELM_CHART_DIR/values.yaml"
-f "$SLOTS_HELM_CHART_DIR/values-master.yaml"
--set image.repository="$IMAGE_REPOSITORY"
--set image.tag="$IMAGE_TAG"
--set app.profile=master
--set app.configLabel=master
+214
View File
@@ -0,0 +1,214 @@
variables:
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
CONFIG_SERVER_SERVICE_DIR: "services/spring-cloud-config-server"
CONFIG_SERVER_HELM_CHART_DIR: "helm/spring-cloud-config-server"
CONFIG_SERVER_HELM_RELEASE: "spring-cloud-config-server"
CONFIG_SERVER_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-cloud-config-server"
CONFIG_SERVER_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
CONFIG_SERVER_K8S_NAMESPACE_DEV: "pcp-dev"
CONFIG_SERVER_GIT_SECRET_NAME: "spring-cloud-config-server-git"
CONFIG_GIT_URI: "https://git.nstart.cloud/2-6/2-6-4/missionis/observatio-terrae.git"
CONFIG_GIT_USERNAME: "gitlab+deploy-token-5"
CONFIG_GIT_DEFAULT_LABEL: "dev"
CONFIG_GIT_SEARCH_PATHS: "config-repo"
CONFIG_GIT_SKIP_SSL_VALIDATION: "true"
.config_server_changes: &config_server_changes
- services/spring-cloud-config-server/**/*
- build.gradle.kts
- settings.gradle.kts
- .gitlab/ci/spring-cloud-config-server.yml
- helm/spring-cloud-config-server/**/*
- deploy/**/*
.config-server:rules:build-and-image:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *config_server_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *config_server_changes
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *config_server_changes
- when: never
.config-server:rules:test:
rules:
- if: '$RUN_ALL == "true"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes: *config_server_changes
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *config_server_changes
when: manual
- if: '$CI_COMMIT_BRANCH == "master"'
changes: *config_server_changes
- when: never
.config-server:rules:deploy-dev:
rules:
- if: '$RUN_ALL == "true"'
when: manual
- if: '$CI_COMMIT_BRANCH == "dev"'
changes: *config_server_changes
when: manual
- when: never
.config-server:gradle-cache:
cache:
key: "gradle-config-server-$CI_DEFAULT_BRANCH"
paths:
- .gradle/wrapper
- .gradle/caches
policy: pull-push
.config-server:docker-build:
image: repo.nstart.cloud/library/docker:26
services:
- name: repo.nstart.cloud/library/docker:26-dind
alias: docker
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_BUILDKIT: "1"
before_script:
- docker version
- docker info
.config-server:helm-deploy:
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
before_script:
- mkdir -p /root/.kube
- helm version
after_script:
- rm -f /root/.kube/config
config-server:test:
stage: test
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .config-server:rules:test
- .config-server:gradle-cache
script:
- gradle --no-daemon :services:spring-cloud-config-server:test
artifacts:
when: always
reports:
junit:
- services/spring-cloud-config-server/build/test-results/test/*.xml
paths:
- services/spring-cloud-config-server/build/reports/tests/
expire_in: 7 days
config-server:build:
stage: build
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
extends:
- .config-server:rules:build-and-image
- .config-server:gradle-cache
needs:
- job: config-server:test
artifacts: true
script:
- gradle --no-daemon :services:spring-cloud-config-server:bootJar
artifacts:
when: always
paths:
- services/spring-cloud-config-server/build/libs/*.jar
expire_in: 7 days
config-server:package:
stage: package
extends:
- .config-server:rules:build-and-image
- .config-server:docker-build
needs:
- job: config-server:build
artifacts: true
script:
- 'echo "CONFIG_SERVER_IMAGE_REPOSITORY=$CONFIG_SERVER_IMAGE_REPOSITORY"'
- |
JAR_COUNT="$(find "$CONFIG_SERVER_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [ "$JAR_COUNT" -ne 1 ]; then
echo "Expected exactly one executable jar, found: $JAR_COUNT"
find "$CONFIG_SERVER_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
exit 1
fi
JAR_FILE="$(find "$CONFIG_SERVER_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
- rm -rf docker-context
- mkdir -p docker-context/build/libs
- cp "$CONFIG_SERVER_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
- cp "$JAR_FILE" docker-context/build/libs/app.jar
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
- docker build --file docker-context/Dockerfile --tag "$CONFIG_SERVER_IMAGE_REPOSITORY:$CONFIG_SERVER_IMAGE_TAG" docker-context
- docker push "$CONFIG_SERVER_IMAGE_REPOSITORY:$CONFIG_SERVER_IMAGE_TAG"
- printf "CONFIG_SERVER_IMAGE=%s\n" "$CONFIG_SERVER_IMAGE_REPOSITORY:$CONFIG_SERVER_IMAGE_TAG" > config-server-image.env
artifacts:
reports:
dotenv: config-server-image.env
expire_in: 7 days
config-server:deploy:dev:
stage: deploy
extends:
- .config-server:rules:deploy-dev
- .config-server:helm-deploy
needs:
- job: config-server:package
artifacts: true
environment:
name: dev/spring-cloud-config-server
script:
- |
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
exit 1
fi
else
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
exit 1
fi
- chmod 600 /root/.kube/config
- export KUBECONFIG=/root/.kube/config
- |
if [ -z "${CONFIG_GIT_TOKEN:-}" ]; then
echo "CONFIG_GIT_TOKEN must be set as a GitLab CI/CD variable."
exit 1
fi
- kubectl create namespace "$CONFIG_SERVER_K8S_NAMESPACE_DEV" --dry-run=client -o yaml | kubectl apply -f -
- >
kubectl create secret generic "$CONFIG_SERVER_GIT_SECRET_NAME"
--namespace "$CONFIG_SERVER_K8S_NAMESPACE_DEV"
--from-literal=token="$CONFIG_GIT_TOKEN"
--dry-run=client
-o yaml
| kubectl apply -f -
- 'export IMAGE_REPOSITORY="${CONFIG_SERVER_IMAGE%:*}"'
- 'export IMAGE_TAG="${CONFIG_SERVER_IMAGE##*:}"'
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
- >
helm upgrade --install "$CONFIG_SERVER_HELM_RELEASE"
"$CONFIG_SERVER_HELM_CHART_DIR"
--namespace "$CONFIG_SERVER_K8S_NAMESPACE_DEV"
--create-namespace
--values "$CONFIG_SERVER_HELM_CHART_DIR/values.yaml"
--values "$CONFIG_SERVER_HELM_CHART_DIR/values-dev.yaml"
--set-string image.repository="$IMAGE_REPOSITORY"
--set-string image.tag="$IMAGE_TAG"
--set-string app.configGitUri="$CONFIG_GIT_URI"
--set-string app.configGitUsername="$CONFIG_GIT_USERNAME"
--set-string app.configGitDefaultLabel="$CONFIG_GIT_DEFAULT_LABEL"
--set-string app.configGitSearchPaths="$CONFIG_GIT_SEARCH_PATHS"
--set-string app.configGitSkipSslValidation="$CONFIG_GIT_SKIP_SSL_VALIDATION"
--set-string gitTokenSecret.name="$CONFIG_SERVER_GIT_SECRET_NAME"
--atomic
--wait
--timeout 15m
+273
View File
@@ -0,0 +1,273 @@
# AGENTS.md
## Назначение
Этот документ задаёт правила работы агента и базовые инженерные правила для изменений в репозитории.
Цель агента:
- вносить минимальные, безопасные и ревьюопригодные изменения;
- сохранять архитектурные границы;
- не ломать существующие контракты без явного запроса;
- следовать уже используемым в репозитории паттернам и стилю.
Этот документ **не** является полным платформенным стандартом для BPM/Kafka. Такие правила вынесены в `docs/standards/PLATFORM_STANDARD.md`.
---
## Дополнительные обязательные документы
При работе в этом репозитории:
- Для изменений, связанных с BPM / Zeebe / Camunda / Kafka / retries / DLQ / compensation / schema evolution, перед изменением кода прочитай `docs/standards/PLATFORM_STANDARD.md`.
- Перед завершением нетривиальных изменений проверь `docs/standards/CHECKLIST.md`.
- Если задача затрагивает только обычный Kotlin/Spring-код без BPM/Kafka-аспектов, достаточно следовать правилам этого `AGENTS.md`.
---
## Обязательный стиль работы
Перед внесением изменений:
1. Найди похожую реализацию в том же модуле, сервисе или соседнем слое.
2. Следуй уже принятому стилю проекта и существующей структуре.
3. Прочитай полный execution path до редактирования кода.
4. Держи решение в правильном архитектурном слое.
5. Сделай минимальное изменение, которое полностью решает задачу.
6. Обнови или добавь тесты для изменённого поведения.
Для нетривиальных задач:
1. Кратко определи затронутые модули и файлы.
2. Явно зафиксируй ключевые допущения, если есть неоднозначность.
3. Предпочитай самый безопасный и наименее сложный вариант вместо редизайна.
Если требования неоднозначны:
- не придумывай редизайн;
- сохраняй текущее поведение, если нет явного запроса на его изменение;
- явно фиксируй допущения;
- предпочитай production safety «красивым» решениям.
---
## Предпочтение в дизайне решения
По умолчанию предпочитай самое простое и понятное решение, которое корректно удовлетворяет текущим требованиям.
Более сложная архитектура, абстракции, дополнительные слои или более общие конструкции допустимы только тогда, когда более простое решение заметно ухудшит хотя бы одно из следующего:
- корректность;
- понятность и поддерживаемость;
- границы ответственности;
- тестируемость;
- надёжность;
- интеграцию с существующей архитектурой.
Не усложняй решение как подготовку к возможным будущим требованиям.
Если есть несколько рабочих вариантов, выбирай наименее сложный, который не ухудшает качество решения.
---
## Базовые предположения по стеку
- Язык: Kotlin
- Framework: Spring Boot
- Возможные интеграции:
- REST
- Kafka
- BPM / Zeebe / Camunda 8
- database persistence
- external HTTP/gRPC APIs
Следуй реальной структуре и библиотекам, уже используемым в репозитории.
Не вводи новый стиль, если в проекте уже есть устоявшийся.
---
## Архитектурные границы
### Ключевой принцип
Сервис владеет доменным состоянием и бизнес-правилами.
### Владение по слоям
- `controller/api` -> только transport
- `application/service/use case` -> orchestration внутри границы сервиса
- `domain` -> бизнес-правила, инварианты, решения
- `repository/adapter/client` -> только persistence и внешняя коммуникация
### Где нельзя размещать бизнес-логику
Нельзя размещать доменную бизнес-логику в:
- controllers
- repository implementations
- Kafka listeners / consumers
- Zeebe workers
- external client adapters
- BPMN process definitions
---
## Правила Kotlin
- Предпочитай явный и читаемый код.
- Делай функции небольшими и отражающими намерение.
- Предпочитай `val` вместо `var`, если мутация не нужна.
- Избегай чрезмерно «умных» chain/scope-function конструкций, если они ухудшают читаемость.
- Используй понятные имена; не вводи аббревиатуры без необходимости.
- Не вводи DSL-подобный или чрезмерно функциональный стиль, если он не доминирует в проекте.
- Сохраняй текущий подход проекта к null-safety.
- Не заменяй явные ошибки на `null`-driven control flow.
- Предпочитай прямой и понятный код вместо «умных», слишком универсальных или академичных решений.
### Правило комментариев для Kotlin/Spring кода
Комментарии обязательны там, где они реально помогают сопровождению кода:
- для публичных функций и публичных контрактов, если их назначение неочевидно из сигнатуры;
- для сложных бизнес-правил, инвариантов и ограничений;
- для retry / compensation / idempotency / transactional логики;
- для неочевидных решений, важных допущений и edge cases;
- для переменных и локальных вычислений только тогда, когда без комментария намерение неясно.
Не нужно комментировать каждую переменную или каждую функцию механически.
Комментарии должны объяснять цель и reasoning, а не пересказывать синтаксис.
Плохой комментарий:
- `// increment counter`
Хороший комментарий:
- `// Хранит количество уже выполненных попыток доставки для этой операции, чтобы retry-логика могла определить, нужно ли повторить обработку или отправить сообщение в DLQ.`
---
## Правила Spring Boot
- Контроллеры должны оставаться тонкими.
- Контроллеры должны только:
- принимать и валидировать вход;
- маппить transport-модели;
- вызывать application/service layer;
- возвращать response.
- Бизнес-правила не должны жить в контроллерах.
- Репозитории не должны содержать бизнес-решения.
- Transaction boundaries должны находиться в service/application layer, если модуль уже не использует другой устоявшийся паттерн.
- Не выполняй долгий remote I/O внутри DB-транзакций без явной необходимости текущего дизайна.
- Переиспользуй существующий стиль Spring-конфигурации, properties, beans, mappers и utilities.
---
## Правила API и error handling
- Сохраняй публичные REST/gRPC контракты, если явно не запрошено иное.
- Переиспользуй существующий response/error format.
- Переиспользуй текущий validation style.
- Держи backward compatibility в фокусе.
- Если контракт меняется, обнови тесты и nearby examples/docs.
- Не используй broad `try/catch`, скрывающий ошибки.
- Не проглатывай исключения.
- Не делай silent downgrade ошибок в дефолтные значения.
- Следуй существующему exception mapping pattern.
- Держи error handling явным и удобным для дебага.
- Избегай дублирующих шумных логов для одного и того же failure path.
---
## Правила observability
Для важных бизнес-флоу сохраняй или улучшай:
- structured logging;
- metrics;
- tracing/correlation;
- operation identifiers.
Если меняется Kafka/BPM/external integration logic:
- сохрани или добавь correlation identifiers;
- сохрани или добавь `operation_id`, где это релевантно;
- логируй достаточно для дебага retries, duplicates и compensations;
- никогда не логируй секреты или sensitive payloads.
---
## Правила тестирования
Всегда обновляй тесты, если меняется поведение.
Добавляй или обновляй unit tests, если меняются:
- business rules;
- branching logic;
- validation;
- idempotency behavior;
- retry/compensation behavior.
Добавляй или обновляй integration tests, если меняются:
- Spring wiring;
- controller behavior;
- persistence behavior;
- Kafka behavior;
- Zeebe worker behavior;
- transaction behavior;
- external integration behavior.
Каждое bug fix-изменение по возможности должно сопровождаться regression test.
---
## Validation commands
Сначала предпочитай самый маленький релевантный прогон, потом более широкий.
Типичные команды:
- `./gradlew test`
- `./gradlew ktlintCheck`
- `./gradlew build`
Если в репозитории есть module-specific команды, используй их, когда это уместно.
Если команда не была запущена:
- точно укажи, какая именно команда не запускалась;
- объясни почему;
- опиши остаточный риск.
---
## Правила контроля scope
Держи изменения узко ограниченными.
Не делай:
- переименование файлов / классов / пакетов без явной необходимости;
- перенос кода между модулями без сильной архитектурной причины;
- переформатирование несвязанных файлов;
- «попутную уборку» соседнего кода без необходимости для задачи;
- введение абстракций «на будущее».
Предпочитай:
- локальные изменения;
- существующие абстракции;
- явные, простые, сопровождаемые решения.
---
## Обязательный формат финального ответа
В конце задачи всегда предоставляй:
1. `Changed files`
- список изменённых файлов
2. `What changed`
- краткое summary реализации
3. `Architecture compliance`
- объясни, почему решение соблюдает layer boundaries
- если участвует BPM или messaging, кратко укажи, как соблюдены платформенные требования из `docs/standards/PLATFORM_STANDARD.md`
4. `Validation`
- какие команды запускались
- результаты
5. `Risks / assumptions`
- что не было полностью проверено
- compatibility considerations
- edge cases
+3 -3
View File
@@ -1,4 +1,4 @@
# observatio-terrae
# pcp
@@ -15,14 +15,14 @@ Already a pro? Just edit this README.md and make it your own. Want to make it ea
```
cd existing_repo
git remote add origin https://git.nstart.cloud/2-6/2-6-4/missionis/observatio-terrae.git
git remote add origin https://git.nstart.local/emelianov.an/pcp.git
git branch -M master
git push -uf origin master
```
## Integrate with your tools
* [Set up project integrations](https://git.nstart.cloud/2-6/2-6-4/missionis/observatio-terrae/-/settings/integrations)
* [Set up project integrations](https://git.nstart.local/emelianov.an/pcp/-/settings/integrations)
## Collaborate with your team
+69
View File
@@ -0,0 +1,69 @@
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
kotlin("jvm") apply false
kotlin("plugin.spring") apply false
kotlin("plugin.jpa") apply false
kotlin("plugin.lombok") apply false
id("org.springframework.boot") apply false
id("io.spring.dependency-management") apply false
//id("org.sonarqube") apply false
}
allprojects {
group = "space.nstart.pcp"
ext.set("kotlinVersion", "2.2.20")
repositories {
// maven {
// url = uri("https://repo.nstart.cloud/repository/maven-proxy/")
// }
mavenCentral {
content {
includeGroup("org.jacoco")
includeGroup("org.ow2.asm")
}
}
mavenCentral()
mavenLocal()
gradlePluginPortal()
maven { url = uri("https://jitpack.io") }
exclusiveContent {
forRepository {
maven("https://repo.osgeo.org/repository/release/")
}
filter {
includeGroup("org.geotools")
includeGroup("org.geotools.ogc")
includeGroup("javax.media")
includeGroup("it.geosolutions.jgridshift")
}
}
}
}
subprojects {
plugins.withId("java") {
extensions.configure<JavaPluginExtension> {
toolchain {
languageVersion = JavaLanguageVersion.of((property("versions.java") as String).toInt())
}
}
}
plugins.withId("org.springframework.boot") {
if (
path != ":services:pcp-srpring-boot-admin-server" &&
path != ":services:spring-cloud-config-server"
) {
dependencies.add(
"implementation",
"de.codecentric:spring-boot-admin-starter-client:${property("versions.spring.boot.admin")}"
)
}
}
}
Executable
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
#
# Сборка bootJar всех Spring-сервисов.
#
# ./buildAll.sh # собрать все jar (без тестов; bootJar тесты не гоняет)
# ./buildAll.sh --clean # clean + сборка с нуля
#
# Фронт pcp-tgu-ui-service здесь не собирается (это npm-проект, не gradle).
# Чтобы после сборки разложить jar в serviceAllBuild/ — запусти ./serviceAllBuild.sh
# (или сразу ./serviceAllBuild.sh --build, который делает сборку и раскладку вместе).
#
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ARGS=()
[[ "${1:-}" == "--clean" ]] && ARGS+=(clean)
ARGS+=(bootJar)
echo "==> ./gradlew ${ARGS[*]}"
"$ROOT/gradlew" "${ARGS[@]}"
echo
echo "Готово. Разложить в одну папку: ./serviceAllBuild.sh"
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <old-ip> <new-ip>"
echo "Example: $0 192.168.60.146 192.168.60.146"
exit 1
fi
OLD_IP="$1"
NEW_IP="$2"
is_valid_ip() {
local ip="$1"
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
IFS='.' read -r a b c d <<< "$ip"
for part in "$a" "$b" "$c" "$d"; do
(( part >= 0 && part <= 255 )) || return 1
done
}
if ! is_valid_ip "$OLD_IP"; then
echo "Invalid old IP: $OLD_IP"
exit 1
fi
if ! is_valid_ip "$NEW_IP"; then
echo "Invalid new IP: $NEW_IP"
exit 1
fi
export OLD_IP
export NEW_IP
changed_files=0
skipped_files=0
while IFS= read -r -d '' file; do
# Пропускаем бинарные файлы
if ! grep -Iq . "$file"; then
skipped_files=$((skipped_files + 1))
continue
fi
if perl -0ne '
BEGIN { $old = quotemeta($ENV{"OLD_IP"}); }
exit 0 if /(?<![0-9.])$old(?![0-9.])/;
exit 1;
' "$file"; then
perl -0pi -e '
BEGIN {
$old = quotemeta($ENV{"OLD_IP"});
$new = $ENV{"NEW_IP"};
}
s/(?<![0-9.])$old(?![0-9.])/$new/g;
' "$file"
echo "changed: $file"
changed_files=$((changed_files + 1))
fi
done < <(
find . \
-path './.git' -prune -o \
-type f \
-print0
)
echo ""
echo "Done."
echo "IP replaced: $OLD_IP -> $NEW_IP"
echo "Files updated: $changed_files"
echo "Files skipped: $skipped_files"
+37
View File
@@ -0,0 +1,37 @@
# Dynamic Slots Change Log
1. pcp-ballistics-service
- Ужесточены уровни логирования для production/runtime окружения.
- Отключены подробные SQL/JDBC/Flyway/Spring Web логи, чтобы уменьшить шум в логах сервиса.
2. pcp-dynamic-plan-service
- Добавлена история запусков расчета динамического плана с возможностью получать список запусков и отдельный запуск.
- Добавлено восстановление незавершенных расчетов после рестарта сервиса: зависшие `PENDING` и `RUNNING` запуски переводятся в `FAILED`.
- Добавлены режимы расчета `FULL` и `GREEDY`.
- Добавлен выбор направления витка для расчета: оба, восходящий или нисходящий.
- Добавлен флаг фильтрации маршрутов по уже покрытой территории.
- Добавлено сохранение построенных интервалов расчета и API для их получения.
- Расширен результат расчета метаданными: режим расчета, режим витка, флаг фильтрации, время последнего маршрута и процент покрытия территории.
- Добавлена greedy-логика выбора маршрутов с учетом ограничений КА, суточной загрузки, загрузки по виткам и конфликтов по времени.
- Добавлена диагностика причин, по которым интервалы не удалось закрыть маршрутом.
- Добавлен параллельный расчет параметров наблюдения по чанкам с настройками размера чанка и parallelism.
- Добавлены debug API для расчета параметров наблюдения, проверки greedy-математики и построения маршрутов по сохраненным параметрам наблюдения.
- Добавлена возможность сохранять debug-результаты параметров наблюдения во временную директорию.
- Generated debug-артефакты `obsParams` исключены из git.
- Добавлены миграции БД для новых параметров запуска и хранения интервалов.
- Обновлены unit/controller tests для новых сценариев расчета, истории запусков, интервалов и debug-функций.
3. pcp-ui-service
- Добавлен просмотр истории запусков динамического плана.
- Добавлено открытие ранее выполненного запуска и просмотр его результата.
- Добавлены UI-контролы для выбора режима расчета, направления витка и фильтрации по покрытой территории.
- Интервал расчета в UI переведен на выбор дат с автоматическим построением границ дня.
- В результат расчета добавлено отображение режима, направления витка, фильтра покрытия, последнего маршрута и процента покрытия территории.
- Добавлены действия для отображения и скрытия результата расчета на карте.
- Добавлен слой карты для отображения интервалов и маршрутов dynamic plan в Cesium.
- Обновлен экран карты, чтобы он подключал и инициализировал слой dynamic plan.
- Обновлены стили страницы dynamic plan под историю запусков, новые параметры расчета и map controls.
- Обновлены тесты UI-контроллера под новые endpoints и поведение.
4. spring-cloud-config-server
- Отключена регистрация config-server в Spring Boot Admin.
+25
View File
@@ -0,0 +1,25 @@
# config-repo
Файлы в этом подпроекте предназначены для `spring-cloud-config-server`.
Ожидаемый сценарий:
- этот каталог хранится в GitLab-репозитории с конфигами
- `spring-cloud-config-server` читает его через `CONFIG_GIT_URI`
- сервисы запрашивают конфиги по имени `spring.application.name`
Для `tle-monitoring-service` используются файлы:
- `tle-monitoring-service.yaml`
Пример запроса к Config Server:
`GET /tle-monitoring-service/local`
Архитектурные договоренности:
- `pcp-request-service` является единственным owner request/grid-домена.
- `pcp.services.request` должен использоваться как основной runtime endpoint для requests/cells/grid.
- `pcp.services.earth-grid` сохранен только как legacy alias и должен резолвиться в `pcp.services.request`.
- lifecycle booked slots должен идти Kafka-first; legacy HTTP `/api/slots/booking/process` не считается рабочим основным путем.
- `pcp.services.admin` указывает на `pcp-srpring-boot-admin-server`; сервисы PCP должны регистрироваться в нем через Spring Boot Admin client.
+62
View File
@@ -0,0 +1,62 @@
# TODO before merge request: вернуть старые адреса окружения.
# pcp.infra.postgres.host: localhost
pcp:
infra:
postgres:
host: localhost
port: 35400
kafka:
host: localhost
port: 19092
network:
host: localhost
ports:
config: 8888
admin: 38889
route-processing: 8080
request: 8080
ballistics: 8080
complex-mission-service: 8080
stations: 8080
slots: 8080
mission: 8080
ui: 8080
tgu: 8080
tle-monitoring: 8080
complex-mission-flow: 8080
coverage-scheme: 8080
satellite-catalog: 8080
dynamic-plan: 8080
gateway: 8080
services:
config: http://${pcp.network.host}:${pcp.ports.config}
admin: http://${pcp.network.host}:${pcp.ports.admin}
route-processing: http://${pcp.network.host}:${pcp.ports.route-processing}
request: http://pcp-request-service:${pcp.ports.request}
earth-grid: ${pcp.services.request}
ballistics: http://pcp-ballistics-service:${pcp.ports.ballistics}
complex-mission-service: http://pcp-complex-mission-service:${pcp.ports.complex-mission-service}
stations: http://pcp-stations-service:${pcp.ports.stations}
slots: http://pcp-slots-service:${pcp.ports.slots}
mission: http://pcp-mission-planing-service:${pcp.ports.mission}
ui: http://${pcp.network.host}:${pcp.ports.ui}
tgu: http://${pcp.network.host}:${pcp.ports.tgu}
tle-monitoring: http://tle-monitoring-service:${pcp.ports.tle-monitoring}
coverage-scheme: http://pcp-coverage-scheme-service:${pcp.ports.coverage-scheme}
satellite-catalog: http://pcp-satellite-catalog-service:${pcp.ports.satellite-catalog}
dynamic-plan: http://pcp-dynamic-plan-service:${pcp.ports.dynamic-plan}
gateway: http://pcp-gateway-service:${pcp.ports.gateway}
spring:
boot:
admin:
client:
enabled: true
url:
- ${pcp.services.admin}
auto-registration: true
auto-deregistration: true
register-once: true
+80
View File
@@ -0,0 +1,80 @@
pcp:
infra:
postgres:
host: postgres
port: 5432
kafka:
host: kafka
port: 9092
network:
host: localhost
ports:
config: 8888
admin: 7000
route-processing: 7004
request: 7005
ballistics: 7003
complex-mission-service: 7002
stations: 7009
slots: 7006
mission: 7010
ui: 7008
tgu: 7011
tle-monitoring: 7001
complex-mission-flow: 7007
coverage-scheme: 7012
satellite-catalog: 7013
dynamic-plan: 7014
gateway: 7080
services:
config: http://spring-cloud-config-server:${pcp.ports.config}
admin: http://pcp-srpring-boot-admin-server:${pcp.ports.admin}
route-processing: http://pcp-route-processing-service:${pcp.ports.route-processing}
request: http://pcp-request-service:${pcp.ports.request}
earth-grid: ${pcp.services.request}
ballistics: http://pcp-ballistics-service:${pcp.ports.ballistics}
complex-mission-service: http://pcp-complex-mission-service:${pcp.ports.complex-mission-service}
stations: http://pcp-stations-service:${pcp.ports.stations}
slots: http://slots-service:${pcp.ports.slots}
mission: http://pcp-mission-planing-service:${pcp.ports.mission}
ui: http://pcp-ui-service:${pcp.ports.ui}
tgu: http://pcp-tgu-service:${pcp.ports.tgu}
tle-monitoring: http://tle-monitoring-service:${pcp.ports.tle-monitoring}
coverage-scheme: http://pcp-coverage-scheme-service:${pcp.ports.coverage-scheme}
satellite-catalog: http://pcp-satellite-catalog-service:${pcp.ports.satellite-catalog}
dynamic-plan: http://pcp-dynamic-plan-service:${pcp.ports.dynamic-plan}
gateway: http://pcp-gateway-service:${pcp.ports.gateway}
spring:
kafka:
admin:
auto-create: true
listener:
missing-topics-fatal: false
camunda:
client:
enabled: ${CAMUNDA_CLIENT_ENABLED:false}
grpc-address: ${CAMUNDA_GRPC_ADDRESS:http://192.168.60.201:26500}
auth:
method: none
prefer-rest-over-grpc: false
planning:
external-update-enabled: false
classifier:
platforms-url: https://ordinis.nstart.cloud/api/v1/spacecraft/records
platforms-cache-ttl: 15m
allowed-platform-statuses:
- OPERATIONAL
tle:
polling:
enabled: ${TLE_POLLING_ENABLED:false}
settings:
ic-service: http://pcp-satellite-catalog-service:${pcp.ports.satellite-catalog}
+64
View File
@@ -0,0 +1,64 @@
# Local profile defaults are intentionally overridable because developer machines
# and local Docker deployments use different network names.
pcp:
infra:
postgres:
host: ${PCP_POSTGRES_HOST:localhost}
port: ${PCP_POSTGRES_PORT:5432}
kafka:
host: ${PCP_KAFKA_HOST:localhost}
port: ${PCP_KAFKA_PORT:29092}
network:
host: ${PCP_NETWORK_HOST:localhost}
ports:
config: 38888
admin: 7000
route-processing: 7004
request: 7005
ballistics: 7003
complex-mission-service: 7002
stations: 7009
slots: 7006
mission: 7010
ui: 7008
tgu: 7011
tle-monitoring: 7001
complex-mission-flow: 7007
coverage-scheme: 7012
satellite-catalog: 7013
dynamic-plan: 7014
gateway: 7080
services:
config: http://${pcp.network.host}:${pcp.ports.config}
admin: http://${pcp.network.host}:${pcp.ports.admin}
route-processing: http://${pcp.network.host}:${pcp.ports.route-processing}
request: http://${pcp.network.host}:${pcp.ports.request}
earth-grid: ${pcp.services.request}
ballistics: http://${pcp.network.host}:${pcp.ports.ballistics}
complex-mission-service: http://${pcp.network.host}:${pcp.ports.complex-mission-service}
stations: http://${pcp.network.host}:${pcp.ports.stations}
slots: http://${pcp.network.host}:${pcp.ports.slots}
mission: http://${pcp.network.host}:${pcp.ports.mission}
ui: http://${pcp.network.host}:${pcp.ports.ui}
tgu: http://${pcp.network.host}:${pcp.ports.tgu}
tle-monitoring: http://${pcp.network.host}:${pcp.ports.tle-monitoring}
coverage-scheme: http://${pcp.network.host}:${pcp.ports.coverage-scheme}
satellite-catalog: http://${pcp.network.host}:${pcp.ports.satellite-catalog}
dynamic-plan: http://${pcp.network.host}:${pcp.ports.dynamic-plan}
gateway: http://${pcp.network.host}:${pcp.ports.gateway}
spring:
boot:
admin:
client:
enabled: true
url:
- ${pcp.services.admin}
auto-registration: true
auto-deregistration: true
register-once: true
+1
View File
@@ -0,0 +1 @@
description = "Configuration repository for Spring Cloud Config Server"
+57
View File
@@ -0,0 +1,57 @@
server:
port: 38080
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
probes:
enabled: true
show-details: always
meteo:
s3:
endpoint: http://minio:9999
region: us-east-1
access-key: ${METEO_S3_ACCESS_KEY}
secret-key: ${METEO_S3_SECRET_KEY}
bucket: meteo
object-prefix: gfs
path-style-access-enabled: true
cloud:
layer:
layer-id: tcdc
title: TCDC — Total Cloud Cover
description: Отображение общей облачности из GFS, загруженной сервисом meteodownloader
source-parameter: TCDC
unit: "%"
min-value: 0.0
max-value: 100.0
render:
derived-prefix: gfs-derived/tcdc
catalog-prefix: gfs-derived/tcdc-catalog
frame-file-name: frame.json
pmtiles-file-name: frame.pmtiles
pmtiles-content-type: application/vnd.pmtiles
tile-format: png
tile-content-type: image/png
tile-size: 256
min-zoom: 0
native-max-zoom: 6
display-max-zoom: 12
chunk-render-parallelism: 8
render-chunk-tile-count: 32
max-in-flight-render-chunks: 20
frame-render-parallelism: 2
render-missing-on-startup: false
startup-render-limit: 4
catalog-cache-seconds: 30
temp-dir: ./meteo-render-temp
render-version: v5-pmtiles-direct-leaflet
single-instance-mode: true
keep-latest-runs: 1
+133
View File
@@ -0,0 +1,133 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.ballistics}
service-path: /swagger/ui
management-base-url: ${pcp.services.ballistics}
health-url: ${pcp.services.ballistics}/actuator/health
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
---
spring:
config:
activate:
on-profile: dev
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp-ballistics-service
auto-offset-reset: earliest
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
app:
kafka:
topics:
satellites: pcp.satellites
settings:
calculation-interval: 7
stations-service: ${pcp.services.stations}
satellite-catalog-service: ${pcp.services.satellite-catalog}
server:
port: 8080
---
spring:
config:
activate:
on-profile: local
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
admin:
auto-create: ${SPRING_KAFKA_ADMIN_AUTO_CREATE:false}
consumer:
group-id: pcp-ballistics-service
auto-offset-reset: earliest
listener:
auto-startup: ${SPRING_KAFKA_LISTENER_AUTO_STARTUP:false}
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_ballistics
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
app:
kafka:
topics:
satellites: pcp.satellites
settings:
calculation-interval: 28
ic-service: http://${pcp.network.host}:9080
stations-service: ${pcp.services.stations}
satellite-catalog-service: ${pcp.services.satellite-catalog}
server:
port: 7003
@@ -0,0 +1,141 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.complex-mission-service}
service-path: /swagger/ui
management-base-url: ${pcp.services.complex-mission-service}
health-url: ${pcp.services.complex-mission-service}/actuator/health
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
complex-plan:
batch-chunk-size: 100
---
spring:
config:
activate:
on-profile: local
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_satellites
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
template:
default-topic: pcp.tle
consumer:
group-id: pcp-complex-mission-service
app:
kafka:
topics:
satellites: pcp.satellites
server:
port: ${pcp.ports.complex-mission-service:7002}
settings:
calculation-interval: 3
ballistics-service: ${pcp.services.ballistics}
request-service: ${pcp.services.request}
earth-grid-service: ${pcp.services.request}
complex-mission-service: ${pcp.services.complex-mission-service}
satellite-catalog-service: ${pcp.services.satellite-catalog}
stations-service: ${pcp.services.stations}
slots-service: ${pcp.services.slots}
coverage-scheme-service: ${pcp.services.coverage-scheme}
---
spring:
config:
activate:
on-profile: dev
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
template:
default-topic: pcp.tle
consumer:
group-id: pcp-complex-mission-service
app:
kafka:
topics:
satellites: pcp.satellites
server:
port: 8080
settings:
calculation-interval: 3
ballistics-service: ${pcp.services.ballistics}
request-service: ${pcp.services.request}
earth-grid-service: ${pcp.services.request}
complex-mission-service: ${pcp.services.complex-mission-service}
satellite-catalog-service: ${pcp.services.satellite-catalog}
stations-service: ${pcp.services.stations}
slots-service: ${pcp.services.slots}
coverage-scheme-service: ${pcp.services.coverage-scheme}
@@ -0,0 +1,72 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.coverage-scheme}
service-path: /swagger/ui
management-base-url: ${pcp.services.coverage-scheme}
health-url: ${pcp.services.coverage-scheme}/actuator/health
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
coverage-scheme:
ballistics-service-url: ${pcp.services.ballistics}
complex-mission-service-url: ${pcp.services.complex-mission-service}
satellite-catalog-service-url: ${pcp.services.satellite-catalog}
default-roll-step-degrees: 5.0
default-minimum-technology-overlap: 0.05
default-allow-partial-coverage: true
boundary-touch-buffer: 0.05
continuity-gap-tolerance: 0.000001
default-orientation-tolerance-degrees: 15.0
max-roll-samples-per-window: 7
flight-line-padding-seconds: 120
web-client-response-timeout-seconds: 30
web-client-in-memory-size-mb: 20
fallback-swath-width-degrees: 1.0
fallback-shift-per-roll-degree: 0.02
default-minimum-time-between-modes-seconds: 0
---
spring:
config:
activate:
on-profile: local
server:
port: ${pcp.ports.coverage-scheme:7012}
---
spring:
config:
activate:
on-profile: dev
server:
port: 8080
+117
View File
@@ -0,0 +1,117 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.dynamic-plan}
service-path: /swagger/ui
management-base-url: ${pcp.services.dynamic-plan}
health-url: ${pcp.services.dynamic-plan}/actuator/health
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
complex-plan:
request-service-url: ${pcp.services.request}
request-selection-path: /v1/requests
ballistics-service-url: ${pcp.services.ballistics}
satellite-catalog-service-url: ${pcp.services.satellite-catalog}
report-dir: ${java.io.tmpdir}/pcp-dynamic-plan-service/reports
interval-coverage-check-enabled: false
geo-layers:
enabled: false
output-dir: ${java.io.tmpdir}/pcp-dynamic-plan-service/geo-layers
---
spring:
config:
activate:
on-profile: local
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_complex_plan
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration/pcp-complex-plan
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
template:
default-topic: pcp.tle
consumer:
group-id: pcp-dynamic-plan-service
app:
kafka:
topics:
satellites: pcp.satellites
server:
port: ${pcp.ports.dynamic-plan:7014}
---
spring:
config:
activate:
on-profile: dev
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration/pcp-complex-plan
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
template:
default-topic: pcp.tle
consumer:
group-id: pcp-dynamic-plan-service
app:
kafka:
topics:
satellites: pcp.satellites
server:
port: 8080
+114
View File
@@ -0,0 +1,114 @@
spring:
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.gateway}
management-base-url: ${pcp.services.gateway}
cloud:
gateway:
server:
# WebFlux-вариант Spring Cloud Gateway (Boot 4 / Spring Cloud 2025.1).
webflux:
# CORS теперь живёт ТОЛЬКО здесь — единая точка на edge. Раньше дублировался
# per-service (pcp-ui-service/WebConfig.kt), но после сворачивания фронта на
# gateway браузер ходит к бэкендам строго через него. Страховка от cross-origin
# к самому gateway (dev напрямую, LAN-адрес рабочей станции): allowedOriginPatterns
# "*" совместим с allowCredentials=true (в отличие от allowedOrigins "*").
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600
routes:
# --- Доменные сервисы. Префикс /api/pcp-<x> СРЕЗАЕТСЯ (StripPrefix=2 убирает
# 2 сегмента: api + pcp-<x>), т.е. /api/pcp-tgu/api/plans -> бэкенд /api/plans.
# Это ровно семантика прежнего vite-rewrite. Цели берём из реестра pcp.services.*.
- id: tgu
uri: ${pcp.services.tgu}
predicates:
- Path=/api/pcp-tgu/**
filters:
- StripPrefix=2
- id: ballistics
uri: ${pcp.services.ballistics}
predicates:
- Path=/api/pcp-ballistics/**
filters:
- StripPrefix=2
- id: mission
uri: ${pcp.services.mission}
predicates:
- Path=/api/pcp-mission/**
filters:
- StripPrefix=2
- id: request
uri: ${pcp.services.request}
predicates:
- Path=/api/pcp-request/**
filters:
- StripPrefix=2
- id: complex
uri: ${pcp.services.complex-mission-service}
predicates:
- Path=/api/pcp-complex/**
filters:
- StripPrefix=2
- id: slots
uri: ${pcp.services.slots}
predicates:
- Path=/api/pcp-slots/**
filters:
- StripPrefix=2
# Каталог КА: фронт сюда не ходит, но slots-service обращается к нему строго
# через gateway (settings.satellite-catalog-service -> /api/pcp-satellite-catalog).
# StripPrefix=2: /api/pcp-satellite-catalog/api/satellites -> бэкенд /api/satellites.
- id: satellite-catalog
uri: ${pcp.services.satellite-catalog}
predicates:
- Path=/api/pcp-satellite-catalog/**
filters:
- StripPrefix=2
# --- Станции: путь СОХРАНЯЕМ (в vite было без rewrite) — без StripPrefix.
- id: stations
uri: ${pcp.services.stations}
predicates:
- Path=/api/stations/**
# Catch-all /api/** -> pcp-ui-service СНЯТ: фронт переведён на источники напрямую
# (tgu/баллистика/mission), ui-service выводится из эксплуатации. См.
# docs/план-перевода-с-pcp-ui-service.md и задачу #40 бэклога. Любой не-матчнутый
# /api/** теперь честно отдаёт 404 на gateway, а не молча уходит в умирающий сервис.
# Валидация JWT на edge. По умолчанию ВЫКЛЮЧЕНА (anonymous, как сейчас работает фронт).
# Для закрытого контура: GATEWAY_JWT_ENABLED=true И задать issuer через env
# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=<realm new-start>.
# Issuer намеренно НЕ объявлен здесь: пустое значение заставило бы автоконфиг поднимать
# JwtDecoder и падать на старте.
gateway:
security:
jwt-enabled: ${GATEWAY_JWT_ENABLED:false}
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
---
spring:
config:
activate:
on-profile: local
server:
port: ${pcp.ports.gateway:7080}
---
spring:
config:
activate:
on-profile: dev
server:
port: ${pcp.ports.gateway:8080}
@@ -0,0 +1,163 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.mission}
service-path: /swagger/ui
management-base-url: ${pcp.services.mission}
health-url: ${pcp.services.mission}/actuator/health
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
# Возвращаем текст причины ошибки клиенту (reason/exception message в поле `message` тела ответа),
# иначе UI видит только HTTP-фразу и не понимает, почему сохранение съёмки/сброса отклонено.
server:
error:
include-message: always
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
---
spring:
config:
activate:
on-profile: local
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
template:
default-topic: pcp.tle
consumer:
group-id: pcp-mission-planing-service
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
enable-auto-commit: false
listener:
missing-topics-fatal: false
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_missions
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
camunda:
client:
mode: self-managed
grpc-address: ${CAMUNDA_GRPC_ADDRESS:http://192.168.60.201:26500}
auth:
method: none
prefer-rest-over-grpc: false
server:
port: ${pcp.ports.mission:7010}
settings:
ballistics-service: ${pcp.services.ballistics}
complex-mission-service: ${pcp.services.complex-mission-service}
satellite-catalog-service: ${pcp.services.satellite-catalog}
stations-service: ${pcp.services.stations}
app:
kafka:
topics:
booked-slots: pcp.slots.status.v1
mode-status-changed: pcp.route.in.v1
satellites: pcp.satellites
---
spring:
config:
activate:
on-profile: dev
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
template:
default-topic: pcp.tle
consumer:
group-id: pcp-mission-planing-service
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
enable-auto-commit: false
listener:
missing-topics-fatal: false
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
camunda:
client:
mode: self-managed
grpc-address: http://camunda.k8s.265.nstart.cloud:30901
auth:
method: none
prefer-rest-over-grpc: false
server:
port: ${pcp.ports.mission:8080}
settings:
ballistics-service: ${pcp.services.ballistics}
complex-mission-service: ${pcp.services.complex-mission-service}
satellite-catalog-service: ${pcp.services.satellite-catalog}
stations-service: ${pcp.services.stations}
app:
kafka:
topics:
booked-slots: pcp.slots.status.v1
mode-status-changed: pcp.route.in.v1
satellites: pcp.satellites
+91
View File
@@ -0,0 +1,91 @@
server:
port: ${pcp.ports.request:7005}
pcp:
outbox:
request-status-topic: pcp.request.status.v1
publish-batch-size: 50
publish-fixed-delay-ms: 5000
max-attempts: 10
initial-backoff-ms: 1000
max-backoff-ms: 60000
request:
expiry:
enabled: true
fixed-delay-ms: 60000
batch-size: 200
max-batches-per-run: 50
spring:
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.request}
service-path: /swagger/ui
management-base-url: ${pcp.services.request}
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp-request-service
auto-offset-reset: latest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
springdoc:
swagger-ui:
path: /swagger/ui
management:
endpoints:
web:
exposure:
include: health,info,metrics
logging:
level:
root: INFO
org.hibernate.SQL: WARN
org.springframework.kafka: INFO
app:
kafka:
topics:
route: pcp.route.georeference.v1
consumer:
route:
dlq-topic: pcp.route.georeference-dlq.v1
max-attempts: 5
backoff-ms: 1000
max-backoff-ms: 30000
grid:
enabled: true
---
spring:
config:
activate:
on-profile: local
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_requests
username: postgres
password: password
@@ -0,0 +1,70 @@
server:
port: ${pcp.ports.route-processing:7004}
springdoc:
swagger-ui:
path: /swagger/ui
spring:
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.route-processing}
service-path: /swagger/ui
management-base-url: ${pcp.services.route-processing}
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_route_processing
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp-route-processing-service
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
enable-auto-commit: false
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
acks: all
retries: 10
properties:
enable.idempotence: true
max.in.flight.requests.per.connection: 5
listener:
missing-topics-fatal: false
ack-mode: manual_immediate
app:
route-passport:
consumer:
retry:
max-attempts: 3
backoff-ms: 1000
max-backoff-ms: 30000
outbox:
enabled: true
batch-size: 50
fixed-delay-ms: 1000
lock-timeout-ms: 60000
initial-backoff-ms: 1000
max-backoff-ms: 60000
kafka:
topics:
input: pcp.request.survey-georeference.v1
input-dlq: pcp.request.survey-georeference-dlq.v1
processed-route: pcp.route.in.v1
route-georeference: pcp.route.georeference.v1
@@ -0,0 +1,106 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.satellite-catalog}
management-base-url: ${pcp.services.satellite-catalog}
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
---
spring:
config:
activate:
on-profile: local
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_satellite_catalog
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
app:
kafka:
topics:
satellites: pcp.satellites
server:
port: ${pcp.ports.satellite-catalog:7013}
---
spring:
config:
activate:
on-profile: dev
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
app:
kafka:
topics:
satellites: pcp.satellites
server:
port: 8080
@@ -0,0 +1,21 @@
server:
port: ${pcp.ports.admin:38889}
spring:
boot:
admin:
client:
enabled: false
management:
endpoints:
web:
exposure:
include: health,info,env,metrics
endpoint:
health:
show-details: always
logging:
level:
de.codecentric.boot.admin: INFO
+90
View File
@@ -0,0 +1,90 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.stations}
management-base-url: ${pcp.services.stations}
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
---
spring:
config:
activate:
on-profile: local
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_stations
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
server:
port: ${pcp.ports.stations:7009}
---
spring:
config:
activate:
on-profile: dev
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
server:
port: ${pcp.ports.stations:8080}
+156
View File
@@ -0,0 +1,156 @@
spring:
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.tgu}
service-path: /swagger/ui
management-base-url: ${pcp.services.tgu}
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
camunda:
client:
mode: self-managed
grpc-address: http://camunda.k8s.265.nstart.local:30901
auth:
method: none
prefer-rest-over-grpc: false
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
# Возвращаем текст причины ошибки клиенту (reason из ResponseStatusException попадает в поле
# `message` тела ответа). Без этого UI видит только HTTP-фразу «Unprocessable Content» и не
# понимает, почему действие отклонено (напр. «план заложен — используйте fork-forward»).
server:
error:
include-message: always
springdoc:
swagger-ui:
path: /swagger/ui
planning:
notification-before-start-minutes: 263
target-plan-duration-minutes: 1440
target-plan-duration-delta-minutes: 240
external-update-enabled: true
external-points-lookahead-days: 10
in-memory-history-days: 14
external-update-fixed-delay-ms: 600000
notification-check-fixed-delay-ms: 30000
min-reissue-lead-minutes: 5
reissue-prep-minutes: 30
worker-lock-ttl-seconds: 300
waiting-decision-grace-minutes: 30
# Заморозка перед стартом: ручная replace-перевыдача плана в свой слот запрещена,
# если до старта осталось меньше этого числа минут (часть «точки невозврата»).
layin-freeze-minutes: 10
topics:
visibility-windows-changed: pcp.tgu.visibility-windows-changed.v1
satellite-plan-decision: pcp.tgu.satellite-plan-decision.v1
layin-failed: pcp.tgu.layin-failed.v1
layin-confirmed: pcp.tgu.layin-confirmed.v1
plan-not-accepted: pcp.tgu.plan-not-accepted.v1
station-params-changed: pcp.tgu.station-params-changed.v1
tgu:
test-controller:
enabled: true
classifier:
platforms-url: https://ordinis.nstart.cloud/api/v1/spacecraft/records
platforms-cache-ttl: 15m
allowed-platform-statuses:
- OPERATIONAL
---
spring:
config:
activate:
on-profile: local
datasource:
driver-class-name: org.postgresql.Driver
url: ${DB_URL:jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_tgu}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:password}
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp-tgu-service
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
external-api:
base-url: ${pcp.services.ballistics}
mission-planing:
base-url: ${pcp.services.mission}
server:
port: ${pcp.ports.tgu:7011}
camunda:
client:
grpc-address: ${CAMUNDA_GRPC_ADDRESS:http://192.168.60.201:26500}
---
spring:
config:
activate:
on-profile: dev
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp-tgu-service
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
admin:
auto-create: true
external-api:
base-url: ${pcp.services.ballistics}
server:
port: ${pcp.ports.tgu:8080}
+47
View File
@@ -0,0 +1,47 @@
server:
port: ${pcp.ports.ui:7008}
shutdown: graceful
ssl:
enabled: false
spring:
lifecycle.timeout-per-shutdown-phase: 40s
thymeleaf.reactive.max-chunk-size: 8192
jackson:
serialization-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.ui}
management-base-url: ${pcp.services.ui}
health-url: ${pcp.services.ui}/actuator/health
codec:
max-in-memory-size: 20MB
web:
resources:
static-locations: classpath:/static/
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
settings:
calculation-interval: 3
request-service: ${pcp.services.request}
earth-grid-service: ${pcp.services.request}
complex-mission-service: ${pcp.services.complex-mission-service}
dynamic-plan-service: ${pcp.services.dynamic-plan}
tgu-service: ${pcp.services.tgu}
satellite-catalog-service: ${pcp.services.satellite-catalog}
ballistics-service: ${pcp.services.ballistics}
tle-monitoring-service: ${pcp.services.tle-monitoring}
stations-service: ${pcp.services.stations}
slots-service: ${pcp.services.slots}
mission-service: ${pcp.services.mission}
coverage-scheme-service: ${pcp.services.coverage-scheme}
+161
View File
@@ -0,0 +1,161 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.slots}
service-path: /swagger/ui
management-base-url: ${pcp.services.slots}
health-url: ${pcp.services.slots}/actuator/health
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
---
spring:
config:
activate:
on-profile: local
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_slots
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
# Все обращения к другим сервисам — строго через gateway (StripPrefix снимает /api/pcp-<x>).
# Напрямую по адресам сервисов slots-service не ходит.
settings:
request-service: ${pcp.services.gateway}/api/pcp-request
earth-grid-service: ${pcp.services.gateway}/api/pcp-request
satellite-catalog-service: ${pcp.services.gateway}/api/pcp-satellite-catalog
app:
kafka:
topics:
booked-slots: pcp.slots.status.v1
satellites: pcp.satellites
pcp:
booked-slots:
timeout-check-interval: 1h
timeout-threshold: 1h
slot-service:
calc:
parallelism: 8
chunk-duration-seconds: 86400
flush-threshold: 40000
stream-batch-size: 2000
source-margin-seconds: 120
server:
port: ${pcp.ports.slots:7006}
---
spring:
config:
activate:
on-profile: dev
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
# Все обращения к другим сервисам — строго через gateway (StripPrefix снимает /api/pcp-<x>).
# Напрямую по адресам сервисов slots-service не ходит.
settings:
request-service: ${pcp.services.gateway}/api/pcp-request
earth-grid-service: ${pcp.services.gateway}/api/pcp-request
satellite-catalog-service: ${pcp.services.gateway}/api/pcp-satellite-catalog
app:
kafka:
topics:
booked-slots: pcp.slots.status.v1
satellites: pcp.satellites
pcp:
booked-slots:
timeout-check-interval: 1h
timeout-threshold: 1h
slot-service:
calc:
parallelism: 4
chunk-duration-seconds: 86400
flush-threshold: 20000
stream-batch-size: 2000
source-margin-seconds: 120
server:
port: ${pcp.ports.slots:8080}
@@ -0,0 +1,60 @@
spring:
boot:
admin:
client:
enabled: true
url:
- ${pcp.services.admin}
auto-registration: true
auto-deregistration: true
register-once: true
instance:
service-base-url: ${pcp.services.config}
management-base-url: ${pcp.services.config}
cloud:
config:
server:
git:
uri: ${CONFIG_GIT_URI:}
default-label: ${CONFIG_GIT_DEFAULT_LABEL:main}
search-paths: ${CONFIG_GIT_SEARCH_PATHS:config-repo}
clone-on-start: ${CONFIG_GIT_CLONE_ON_START:false}
force-pull: ${CONFIG_GIT_FORCE_PULL:true}
timeout: ${CONFIG_GIT_TIMEOUT:10}
username: ${CONFIG_GIT_USERNAME:oauth2}
password: ${CONFIG_GIT_TOKEN:}
skipSslValidation: ${CONFIG_GIT_SKIP_SSL_VALIDATION:false}
server:
port: ${SERVER_PORT:8888}
management:
endpoints:
web:
exposure:
include: health,info,env,refresh
endpoint:
health:
show-details: always
logging:
level:
org.springframework.cloud.config.server: INFO
---
spring:
config:
activate:
on-profile: local
---
spring:
config:
activate:
on-profile: test
cloud:
config:
server:
git:
uri: https://gitlab.example.invalid/group/config-repo.git
clone-on-start: false
+127
View File
@@ -0,0 +1,127 @@
spring:
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
boot:
admin:
client:
instance:
service-base-url: ${pcp.services.tle-monitoring}
management-base-url: ${pcp.services.tle-monitoring}
codec:
max-in-memory-size: 20MB
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
root: INFO
tle:
polling:
enabled: false
timeout-seconds: 10800
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
---
spring:
config:
activate:
on-profile: dev
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp
auto-offset-reset: earliest
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: "${SPRING_DATASOURCE_URL}"
username: "${SPRING_DATASOURCE_USERNAME}"
password: "${SPRING_DATASOURCE_PASSWORD}"
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
app:
kafka:
topics:
satellites: pcp.satellites
settings:
complex-mission-service: ${pcp.services.complex-mission-service}
satellite-catalog-service: ${pcp.services.satellite-catalog}
server:
port: 8080
---
spring:
config:
activate:
on-profile: local
kafka:
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
consumer:
group-id: pcp
auto-offset-reset: earliest
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_tle
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
app:
kafka:
topics:
satellites: pcp.satellites
settings:
complex-mission-service: ${pcp.services.complex-mission-service}
satellite-catalog-service: ${pcp.services.satellite-catalog}
server:
port: ${pcp.ports.tle-monitoring:7001}
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
./create-chatgpt-archive.sh [archive.zip]
./create-chatgpt-archive.sh --list
Creates a zip archive with project files useful for code analysis.
Environment:
MAX_FILE_SIZE_BYTES Per-file size limit. Default: 1048576
The script includes source, configuration, build metadata, CI, Helm and docs.
It excludes build outputs, logs, local caches, IDE files, archives, binaries and env files.
USAGE
}
mode="create"
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ "${1:-}" == "--list" ]]; then
mode="list"
shift
fi
if (( $# > 1 )); then
usage >&2
exit 2
fi
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
archive_arg="${1:-pcp-chatgpt-context.zip}"
max_file_size_bytes="${MAX_FILE_SIZE_BYTES:-1048576}"
if [[ "$archive_arg" = /* ]]; then
archive_path="$archive_arg"
else
archive_path="$project_root/$archive_arg"
fi
include_paths=(
"AGENTS.md"
"README.md"
"HELP.md"
".dockerignore"
".gitattributes"
".gitignore"
".gitlab-ci.yml"
"build.gradle.kts"
"settings.gradle.kts"
"gradle.properties"
"gradlew"
"gradlew.bat"
"create-chatgpt-archive.sh"
"changeLogsDynamicSlots.md"
".gitlab"
"config-repo"
"docs"
"gradle"
"helm"
"libs"
"schemes"
"services"
)
should_skip_path() {
local path="$1"
case "$path" in
.git|.git/*|.gradle|.gradle/*|.gradle-local|.gradle-local/*|.idea|.idea/*|.kotlin|.kotlin/*|.vscode|.vscode/*)
return 0
;;
build|build/*|logs|logs/*|out|out/*|bin|bin/*|target|target/*|node_modules|node_modules/*)
return 0
;;
*/build|*/build/*|*/logs|*/logs/*|*/out|*/out/*|*/bin|*/bin/*|*/target|*/target/*|*/node_modules|*/node_modules/*)
return 0
;;
esac
case "$path" in
.env|.env.*|*/.env|*/.env.*)
return 0
;;
*.log|*.tmp|*.bak|*.orig|*.class|*.jar|*.war|*.ear|*.zip|*.tar|*.tgz|*.tar.gz|*.gz|*.7z|*.rar)
return 0
;;
*.png|*.jpg|*.jpeg|*.gif|*.webp|*.ico|*.pdf|*.doc|*.docx|*.xls|*.xlsx|*.ppt|*.pptx)
return 0
;;
*.keystore|*.jks|*.p12|*.pem|*.key|*.crt)
return 0
;;
esac
return 1
}
is_allowed_file() {
local path="$1"
case "$path" in
AGENTS.md|README.md|HELP.md|.dockerignore|.gitattributes|.gitignore|.gitlab-ci.yml)
return 0
;;
build.gradle.kts|settings.gradle.kts|gradle.properties|gradlew|gradlew.bat|create-chatgpt-archive.sh|changeLogsDynamicSlots.md)
return 0
;;
Dockerfile|Makefile|Jenkinsfile)
return 0
;;
*.kt|*.kts|*.java|*.groovy|*.xml|*.yaml|*.yml|*.properties|*.json|*.toml|*.md|*.txt)
return 0
;;
*.sql|*.graphql|*.graphqls|*.proto|*.bpmn|*.dmn|*.tpl|*.sh)
return 0
;;
*.html|*.css|*.scss|*.js|*.jsx|*.ts|*.tsx|*.vue)
return 0
;;
esac
return 1
}
manifest_file="$(mktemp)"
trap 'rm -f "$manifest_file"' EXIT
collect_files() {
local root_path
local relative_path
local file_size
for root_path in "${include_paths[@]}"; do
if [[ ! -e "$project_root/$root_path" ]]; then
continue
fi
if [[ -f "$project_root/$root_path" ]]; then
printf '%s\n' "$root_path"
else
(cd "$project_root" && find "$root_path" -type f -print)
fi
done | while IFS= read -r relative_path; do
relative_path="${relative_path#./}"
if should_skip_path "$relative_path"; then
continue
fi
if ! is_allowed_file "$relative_path"; then
continue
fi
file_size="$(stat -c '%s' "$project_root/$relative_path")"
if (( file_size > max_file_size_bytes )); then
continue
fi
printf '%s\n' "$relative_path"
done | sort -u
}
collect_files > "$manifest_file"
file_count="$(wc -l < "$manifest_file" | tr -d ' ')"
if (( file_count == 0 )); then
echo "No files selected for archive." >&2
exit 1
fi
if [[ "$mode" == "list" ]]; then
cat "$manifest_file"
echo
echo "Selected files: $file_count" >&2
exit 0
fi
if ! command -v zip >/dev/null 2>&1; then
echo "zip command is required but was not found." >&2
exit 1
fi
mkdir -p "$(dirname "$archive_path")"
rm -f "$archive_path"
(cd "$project_root" && zip -q -X "$archive_path" -@ < "$manifest_file")
archive_size="$(du -h "$archive_path" | awk '{print $1}')"
echo "Archive created: $archive_path"
echo "Selected files: $file_count"
echo "Archive size: $archive_size"
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -euo pipefail
NAMESPACE="pcp-dev"
POSTGRES_POD="pcp-postgres-0"
validate_db_name() {
local db_name="$1"
if [[ ! "$db_name" =~ ^[a-z][a-z0-9_]*$ ]]; then
echo "ERROR: invalid database name: $db_name" >&2
echo "Allowed: lowercase letters, digits, underscore; must start with a letter." >&2
exit 1
fi
}
secret_name_for_db() {
local db_name="$1"
echo "${db_name//_/-}-db"
}
user_name_for_db() {
local db_name="$1"
echo "${db_name}_user"
}
get_or_create_password() {
local secret_name="$1"
if kubectl -n "$NAMESPACE" get secret "$secret_name" >/dev/null 2>&1; then
kubectl -n "$NAMESPACE" get secret "$secret_name" \
-o jsonpath='{.data.SPRING_DATASOURCE_PASSWORD}' | base64 -d
else
openssl rand -base64 32 | tr -d '/+=' | head -c 32
fi
}
create_db_user_and_secret() {
local db_name="$1"
validate_db_name "$db_name"
local db_user
local secret_name
local db_password
db_user="$(user_name_for_db "$db_name")"
secret_name="$(secret_name_for_db "$db_name")"
db_password="$(get_or_create_password "$secret_name")"
echo
echo "============================================================"
echo "Creating database package"
echo " database: $db_name"
echo " user: $db_user"
echo " secret: $secret_name"
echo "============================================================"
echo "1/4 Create or update PostgreSQL user and database"
kubectl -n "$NAMESPACE" exec -i "$POSTGRES_POD" -- psql \
-U postgres \
-d postgres <<SQL
DO \$\$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_roles WHERE rolname = '$db_user'
) THEN
CREATE ROLE "$db_user" LOGIN PASSWORD '$db_password';
ELSE
ALTER ROLE "$db_user" WITH LOGIN PASSWORD '$db_password';
END IF;
END
\$\$;
SELECT 'CREATE DATABASE "$db_name" OWNER "$db_user"'
WHERE NOT EXISTS (
SELECT FROM pg_database WHERE datname = '$db_name'
) \gexec
ALTER DATABASE "$db_name" OWNER TO "$db_user";
GRANT CONNECT ON DATABASE "$db_name" TO "$db_user";
SQL
echo "2/4 Enable PostGIS"
kubectl -n "$NAMESPACE" exec -i "$POSTGRES_POD" -- psql \
-U postgres \
-d "$db_name" <<SQL
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
ALTER SCHEMA public OWNER TO "$db_user";
GRANT USAGE, CREATE ON SCHEMA public TO "$db_user";
SQL
echo "3/4 Create or update Kubernetes Secret"
kubectl -n "$NAMESPACE" create secret generic "$secret_name" \
--from-literal=SPRING_DATASOURCE_USERNAME="$db_user" \
--from-literal=SPRING_DATASOURCE_PASSWORD="$db_password" \
--dry-run=client -o yaml | kubectl apply -f -
echo "4/4 Check connection and PostGIS"
kubectl -n "$NAMESPACE" exec -i "$POSTGRES_POD" -- env PGPASSWORD="$db_password" psql \
-h localhost \
-U "$db_user" \
-d "$db_name" \
-c 'select current_database(), current_user, postgis_full_version();'
echo "Done: $db_name"
}
if [[ "$#" -eq 0 ]]; then
echo "Usage: $0 <db_name> [db_name...]"
exit 1
fi
for db_name in "$@"; do
create_db_user_and_secret "$db_name"
done
echo
echo "All requested databases processed."
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env bash
set -euo pipefail
NAMESPACE="${NAMESPACE:-pcp-dev}"
POSTGRES_SERVICE="${POSTGRES_SERVICE:-pcp-postgres}"
CLUSTER_DOMAIN="${CLUSTER_DOMAIN:-altum.local}"
POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgis/postgis:15-3.3}"
ADMIN_SECRET="${ADMIN_SECRET:-pcp-postgres-admin}"
ADMIN_PASSWORD_KEY="${ADMIN_PASSWORD_KEY:-POSTGRES_PASSWORD}"
POSTGRES_HOST="${POSTGRES_SERVICE}.${NAMESPACE}.svc.${CLUSTER_DOMAIN}"
usage() {
cat <<EOF
Usage:
$0 <db_name> [db_name...]
$0 -f databases.txt
Examples:
$0 pcp_satellite_catalog pcp_ballistics
cat > databases.txt <<LIST
pcp_satellite_catalog
pcp_ballistics
pcp_requests
LIST
$0 -f databases.txt
Environment overrides:
NAMESPACE=pcp-dev
POSTGRES_SERVICE=pcp-postgres
CLUSTER_DOMAIN=altum.local
POSTGRES_IMAGE=postgis/postgis:15-3.3
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "ERROR: required command not found: $1" >&2
exit 1
fi
}
validate_db_name() {
local db_name="$1"
if [[ ! "$db_name" =~ ^[a-z][a-z0-9_]*$ ]]; then
echo "ERROR: invalid database name: $db_name" >&2
echo "Allowed format: lowercase letters, digits, underscore; must start with a letter." >&2
exit 1
fi
}
secret_name_for_db() {
local db_name="$1"
local dashed="${db_name//_/-}"
echo "${dashed}-db"
}
user_name_for_db() {
local db_name="$1"
echo "${db_name}_user"
}
make_pod_name() {
local prefix="$1"
local db_name="$2"
local safe="${db_name//_/-}"
safe="${safe:0:35}"
echo "${prefix}-${safe}-${RANDOM}${RANDOM}"
}
get_admin_password() {
kubectl -n "$NAMESPACE" get secret "$ADMIN_SECRET" \
-o jsonpath="{.data.${ADMIN_PASSWORD_KEY}}" | base64 -d
}
get_or_create_service_password() {
local secret_name="$1"
if kubectl -n "$NAMESPACE" get secret "$secret_name" >/dev/null 2>&1; then
kubectl -n "$NAMESPACE" get secret "$secret_name" \
-o jsonpath='{.data.SPRING_DATASOURCE_PASSWORD}' | base64 -d
else
openssl rand -base64 32 | tr -d '/+=' | head -c 32
fi
}
create_database_and_user() {
local db_name="$1"
local db_user="$2"
local db_password="$3"
local admin_password="$4"
local pod_name
pod_name="$(make_pod_name "psql-create" "$db_name")"
echo "Creating/updating database and user:"
echo " database: $db_name"
echo " user: $db_user"
kubectl -n "$NAMESPACE" run --rm -i "$pod_name" \
--image="$POSTGRES_IMAGE" \
--restart=Never \
--env="PGPASSWORD=$admin_password" \
--command -- psql \
-h "$POSTGRES_HOST" \
-U postgres \
-d postgres <<SQL
DO \$\$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_roles WHERE rolname = '$db_user'
) THEN
CREATE ROLE "$db_user" LOGIN PASSWORD '$db_password';
ELSE
ALTER ROLE "$db_user" WITH LOGIN PASSWORD '$db_password';
END IF;
END
\$\$;
SELECT 'CREATE DATABASE "$db_name" OWNER "$db_user"'
WHERE NOT EXISTS (
SELECT FROM pg_database WHERE datname = '$db_name'
) \gexec
ALTER DATABASE "$db_name" OWNER TO "$db_user";
GRANT CONNECT ON DATABASE "$db_name" TO "$db_user";
SQL
}
enable_postgis() {
local db_name="$1"
local db_user="$2"
local admin_password="$3"
local pod_name
pod_name="$(make_pod_name "psql-postgis" "$db_name")"
echo "Enabling PostGIS:"
echo " database: $db_name"
kubectl -n "$NAMESPACE" run --rm -i "$pod_name" \
--image="$POSTGRES_IMAGE" \
--restart=Never \
--env="PGPASSWORD=$admin_password" \
--command -- psql \
-h "$POSTGRES_HOST" \
-U postgres \
-d "$db_name" <<SQL
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
ALTER SCHEMA public OWNER TO "$db_user";
GRANT USAGE, CREATE ON SCHEMA public TO "$db_user";
SQL
}
create_or_update_secret() {
local db_name="$1"
local db_user="$2"
local db_password="$3"
local secret_name="$4"
echo "Creating/updating Kubernetes Secret:"
echo " secret: $secret_name"
kubectl -n "$NAMESPACE" create secret generic "$secret_name" \
--from-literal=SPRING_DATASOURCE_USERNAME="$db_user" \
--from-literal=SPRING_DATASOURCE_PASSWORD="$db_password" \
--dry-run=client -o yaml | kubectl apply -f -
}
check_database() {
local db_name="$1"
local db_user="$2"
local db_password="$3"
local pod_name
pod_name="$(make_pod_name "psql-check" "$db_name")"
echo "Checking database:"
echo " database: $db_name"
echo " user: $db_user"
kubectl -n "$NAMESPACE" run --rm -i "$pod_name" \
--image="$POSTGRES_IMAGE" \
--restart=Never \
--env="PGPASSWORD=$db_password" \
--command -- psql \
-h "$POSTGRES_HOST" \
-U "$db_user" \
-d "$db_name" \
-c 'select current_database(), current_user, postgis_full_version();'
}
read_db_list() {
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
fi
if [[ "$1" == "-f" || "$1" == "--file" ]]; then
if [[ $# -ne 2 ]]; then
echo "ERROR: file path is required after $1" >&2
exit 1
fi
local file="$2"
if [[ ! -f "$file" ]]; then
echo "ERROR: file not found: $file" >&2
exit 1
fi
grep -vE '^\s*($|#)' "$file"
else
printf '%s\n' "$@"
fi
}
main() {
require_command kubectl
require_command openssl
require_command base64
echo "Namespace: $NAMESPACE"
echo "Postgres host: $POSTGRES_HOST"
echo "Postgres image: $POSTGRES_IMAGE"
echo "Admin secret: $ADMIN_SECRET"
echo
local admin_password
admin_password="$(get_admin_password)"
mapfile -t databases < <(read_db_list "$@")
if [[ "${#databases[@]}" -eq 0 ]]; then
echo "ERROR: database list is empty" >&2
exit 1
fi
for db_name in "${databases[@]}"; do
validate_db_name "$db_name"
local db_user
local secret_name
local db_password
local jdbc_url
db_user="$(user_name_for_db "$db_name")"
secret_name="$(secret_name_for_db "$db_name")"
db_password="$(get_or_create_service_password "$secret_name")"
jdbc_url="jdbc:postgresql://${POSTGRES_HOST}:5432/${db_name}"
echo
echo "============================================================"
echo "Processing database: $db_name"
echo "============================================================"
create_database_and_user "$db_name" "$db_user" "$db_password" "$admin_password"
enable_postgis "$db_name" "$db_user" "$admin_password"
create_or_update_secret "$db_name" "$db_user" "$db_password" "$secret_name"
check_database "$db_name" "$db_user" "$db_password"
echo
echo "Done:"
echo " database: $db_name"
echo " user: $db_user"
echo " secret: $secret_name"
echo " JDBC URL: $jdbc_url"
done
echo
echo "All databases processed successfully."
}
main "$@"
+26
View File
@@ -0,0 +1,26 @@
pcp_ballistics
pcp_satellites
pcp_complex_plan
pcp_missions
pcp_requests
pcp_route_processing
pcp_satellite_catalog +
pcp_slots
pcp_stations
pcp_tgu
pcp_tle
pcp_ballistics pcp_satellites pcp_complex_plan pcp_missions pcp_requests pcp_route_processing pcp_satellite_catalog pcp_slots pcp_stations pcp_tgu pcp_tle
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail
# PostgreSQL connection settings. Override via env if needed:
# PGHOST=... PGPORT=... PGUSER=... PGPASSWORD=... ./create_pcp_databases.sh
PGHOST="${PGHOST:-localhost}"
PGPORT="${PGPORT:-5432}"
PGUSER="${PGUSER:-postgres}"
PGPASSWORD="${PGPASSWORD:-password}"
export PGPASSWORD
DATABASES=(
pcp_ballistics
pcp_complex_plan
pcp_missions
pcp_requests
pcp_satellite_catalog
pcp_satellites
pcp_slots
pcp_stations
pcp_tgu
pcp_tle
)
# Services whose migrations use PostGIS geometry/extension.
POSTGIS_DATABASES=(
pcp_ballistics
pcp_missions
pcp_requests
pcp_slots
pcp_stations
)
psql_admin() {
psql -v ON_ERROR_STOP=1 \
--host="$PGHOST" \
--port="$PGPORT" \
--username="$PGUSER" \
--dbname=postgres \
"$@"
}
psql_db() {
local db="$1"
shift
psql -v ON_ERROR_STOP=1 \
--host="$PGHOST" \
--port="$PGPORT" \
--username="$PGUSER" \
--dbname="$db" \
"$@"
}
create_database_if_missing() {
local db="$1"
local exists
exists="$(psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname = '$db'")"
if [[ "$exists" == "1" ]]; then
echo "Database already exists: $db"
else
echo "Creating database: $db"
psql_admin -c "CREATE DATABASE \"$db\" OWNER \"$PGUSER\" ENCODING 'UTF8';"
fi
}
enable_postgis_if_possible() {
local db="$1"
echo "Enabling PostGIS in database: $db"
psql_db "$db" -c "CREATE EXTENSION IF NOT EXISTS postgis;"
}
main() {
echo "Target PostgreSQL: ${PGHOST}:${PGPORT}, user=${PGUSER}"
for db in "${DATABASES[@]}"; do
create_database_if_missing "$db"
done
for db in "${POSTGIS_DATABASES[@]}"; do
enable_postgis_if_possible "$db"
done
echo "Done. Databases are ready. Services will create their schemas via Flyway on startup."
}
main "$@"
+52
View File
@@ -0,0 +1,52 @@
COMPOSE_PROJECT_NAME=pcp-local
USE_EXTERNAL_INFRA=false
PCP_DOCKER_NETWORK=pcp-local_default
PCP_DOCKER_NETWORK_EXTERNAL=false
# Existing allinone infra example:
# USE_EXTERNAL_INFRA=true
# PCP_DOCKER_NETWORK=allinone_camunda
# PCP_DOCKER_NETWORK_EXTERNAL=true
# EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1
# EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1
# CAMUNDA_CLIENT_ENABLED=true
# CAMUNDA_GRPC_ADDRESS=http://orchestration:26500
PCP_IMAGE_TAG=local
POSTGRES_IMAGE=postgis/postgis:16-3.4
KAFKA_IMAGE=apache/kafka:3.7.2
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_PORT=5432
PCP_POSTGRES_HOST=postgres
PCP_POSTGRES_PORT=5432
EXTERNAL_POSTGRES_CONTAINER=
KAFKA_PORT=29092
PCP_KAFKA_HOST=kafka
PCP_KAFKA_PORT=9092
EXTERNAL_KAFKA_CONTAINER=
CAMUNDA_CLIENT_ENABLED=false
CAMUNDA_GRPC_ADDRESS=http://host.docker.internal:26500
CONFIG_SERVER_PORT=38888
PCP_ADMIN_PORT=7000
PCP_COMPLEX_MISSION_PORT=7002
PCP_BALLISTICS_PORT=7003
PCP_ROUTE_PROCESSING_PORT=7004
PCP_REQUEST_PORT=7005
PCP_SLOTS_PORT=7006
PCP_UI_PORT=7008
PCP_STATIONS_PORT=7009
PCP_MISSION_PORT=7010
PCP_TGU_PORT=7011
PCP_COVERAGE_SCHEME_PORT=7012
PCP_SATELLITE_CATALOG_PORT=7013
PCP_DYNAMIC_PLAN_PORT=7014
CONFIG_REPO_DIR=./config-repo
+317
View File
@@ -0,0 +1,317 @@
# PCP local Docker bundle
Локальный bundle предназначен для запуска PCP на машине с Docker без GitLab Config Repo, Kubernetes и Helm.
В bundle входят backend-сервисы PCP, кроме `tle-monitoring-service`; отдельный frontend
`pcp-tgu-ops-ui` также не включается.
## Состав
- `docker-compose.yml` - локальная схема запуска.
- `.env.example` - пример параметров запуска.
- `.env` - создаётся сборочным скриптом в итоговом архиве.
- `config-repo/` - локальный Spring Cloud Config repository.
- `images/` - Docker images, сохранённые через `docker save`.
- `scripts/import-images.sh` - импорт Docker images и запуск базовых сервисов.
- `scripts/init-db.sh` - создание баз данных в одном Postgres контейнере.
- `scripts/start.sh` - запуск инфраструктуры, config-server и приложений.
- `scripts/stop.sh` - остановка compose stack.
- `scripts/status.sh` - статус контейнеров.
## Локальный запуск без архива
Для запуска на той же машине, где собираются images, архив и `docker save` не нужны.
Используйте один скрипт:
```bash
./deploy/local-bundle/run-local.sh start
```
Скрипт:
- создаёт `.env` из `.env.example`, если его ещё нет;
- собирает `bootJar` в Gradle offline mode;
- собирает локальные Docker images `pcp/<service>:<PCP_IMAGE_TAG>`;
- запускает `docker compose`.
Для вашего allinone Postgres/Kafka/Camunda перед первым запуском настройте
`deploy/local-bundle/.env`:
```env
USE_EXTERNAL_INFRA=true
PCP_DOCKER_NETWORK=allinone_camunda
PCP_DOCKER_NETWORK_EXTERNAL=true
PCP_POSTGRES_HOST=postgres
PCP_POSTGRES_PORT=5432
PCP_KAFKA_HOST=kafka
PCP_KAFKA_PORT=9092
EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1
EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1
CAMUNDA_CLIENT_ENABLED=true
CAMUNDA_GRPC_ADDRESS=http://orchestration:26500
```
Команды:
```bash
./deploy/local-bundle/run-local.sh start
./deploy/local-bundle/run-local.sh status
./deploy/local-bundle/run-local.sh logs
./deploy/local-bundle/run-local.sh stop
```
## Пересборка одного сервиса
После первого запуска всего стека можно пересобирать и перезапускать только один сервис:
```bash
./deploy/local-bundle/services/pcp-request-service.sh
```
Это выполнит:
- `bootJar` только для `pcp-request-service`;
- `docker build` только для `pcp/pcp-request-service:<PCP_IMAGE_TAG>`;
- `docker compose up -d --no-deps --force-recreate pcp-request-service`.
Доступны wrapper-скрипты в `deploy/local-bundle/services/` для каждого сервиса.
То же самое можно вызвать через общий скрипт:
```bash
./deploy/local-bundle/run-service.sh pcp-request-service
./deploy/local-bundle/run-service.sh pcp-request-service build
./deploy/local-bundle/run-service.sh pcp-request-service logs
./deploy/local-bundle/run-service.sh pcp-request-service stop
```
## Сборка архива
Из корня репозитория:
```bash
./deploy/local-bundle/build-local-bundle.sh
```
Можно задать версию:
```bash
LOCAL_BUNDLE_VERSION=1.0.0-local ./deploy/local-bundle/build-local-bundle.sh
```
Результат будет создан в:
```bash
deploy/local-bundle/build/pcp-local-bundle-<version>.tar.gz
```
Скрипт сборки:
- собирает `bootJar` для разворачиваемых сервисов;
- запускает Gradle в offline mode по умолчанию;
- собирает Docker images сервисов;
- сохраняет PCP service images в один `images/pcp-services.tar`;
- по умолчанию не сохраняет публичные infra images, чтобы не раздувать архив;
- копирует `docker-compose.yml`, `config-repo`, scripts и документацию;
- формирует итоговый `tar.gz`.
Публичные images `postgis/postgis` и `apache/kafka` будут скачаны на целевой машине командой `docker compose up`, если их нет локально.
Если нужен полностью offline bundle, включите сохранение infra images:
```bash
INCLUDE_INFRA_IMAGES=true ./deploy/local-bundle/build-local-bundle.sh
```
По умолчанию сборка `bootJar` выполняется последовательно, чтобы не упираться в heap Kotlin compiler daemon на локальной машине. Параметры можно переопределить:
```bash
GRADLE_MAX_WORKERS=1 \
GRADLE_JVMARGS="-Xmx2g -Dfile.encoding=UTF-8" \
KOTLIN_DAEMON_JVMARGS="-Xmx3g -XX:MaxMetaspaceSize=768m" \
./deploy/local-bundle/build-local-bundle.sh
```
Gradle offline mode включён по умолчанию:
```bash
GRADLE_OFFLINE=true ./deploy/local-bundle/build-local-bundle.sh
```
Если нужно один раз прогреть локальный Gradle cache, временно отключите offline mode:
```bash
GRADLE_OFFLINE=false ./deploy/local-bundle/build-local-bundle.sh
```
Важно: `GRADLE_OFFLINE=true` отключает сетевой резолв зависимостей Gradle, но Docker всё равно
должен иметь локально доступный base image. Для local bundle сервисные Dockerfile собираются с
локальным override:
```text
LOCAL_BUNDLE_RUNTIME_IMAGE=bellsoft/liberica-openjre-alpine:21.0.5
```
Поэтому перед полностью offline Docker-сборкой подтяните или импортируйте:
```bash
docker pull bellsoft/liberica-openjre-alpine:21.0.5
```
Для полностью переносимого архива с infra images заранее подтяните Postgres/Kafka и включите их
сохранение:
```bash
docker pull postgis/postgis:16-3.4
docker pull apache/kafka:3.7.2
INCLUDE_INFRA_IMAGES=true ./deploy/local-bundle/build-local-bundle.sh
```
## Запуск на локальной машине
Распаковать архив:
```bash
tar -xzf pcp-local-bundle-<version>.tar.gz
cd pcp-local-bundle-<version>
```
Проверить `.env` и при необходимости изменить порты.
Если Postgres/Kafka/Camunda уже подняты внешним allinone compose, например в сети
`allinone_camunda`, включите external infra mode в `.env`:
```env
USE_EXTERNAL_INFRA=true
PCP_DOCKER_NETWORK=allinone_camunda
PCP_DOCKER_NETWORK_EXTERNAL=true
PCP_POSTGRES_HOST=postgres
PCP_POSTGRES_PORT=5432
PCP_KAFKA_HOST=kafka
PCP_KAFKA_PORT=9092
EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1
EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1
CAMUNDA_CLIENT_ENABLED=true
CAMUNDA_GRPC_ADDRESS=http://orchestration:26500
```
В этом режиме `scripts/start.sh` не поднимает свои `postgres` и `kafka`, а сервисы PCP
подключаются к уже существующим контейнерам через общую Docker network.
Импортировать images:
```bash
./scripts/import-images.sh
```
Создать базы данных:
```bash
./scripts/init-db.sh
```
Запустить приложение:
```bash
./scripts/start.sh
```
`start.sh` также запускает `init-db.sh`, если не задано `SKIP_DB_INIT=true`.
## Остановка и статус
```bash
./scripts/status.sh
./scripts/stop.sh
```
Чтобы удалить volumes с данными Postgres/Kafka:
```bash
./scripts/stop.sh -v
```
## Открытые порты
- Config Server: `http://localhost:38888`
- Spring Boot Admin: `http://localhost:7000`
- Complex Mission: `http://localhost:7002`
- Ballistics: `http://localhost:7003`
- Route Processing: `http://localhost:7004`
- Request Service: `http://localhost:7005`
- Slots Service: `http://localhost:7006`
- UI: `http://localhost:7008`
- Stations: `http://localhost:7009`
- Mission Planning: `http://localhost:7010`
- TGU: `http://localhost:7011`
- Coverage Scheme: `http://localhost:7012`
- Satellite Catalog: `http://localhost:7013`
- Dynamic Plan: `http://localhost:7014`
- Postgres: `localhost:5432`
- Kafka external listener: `localhost:29092`
## Проверка готовности
Config Server:
```bash
curl http://localhost:38888/actuator/health
curl http://localhost:38888/pcp-ui-service/local,docker-local
```
UI:
```bash
curl http://localhost:7008/actuator/health
```
Статус контейнеров:
```bash
./scripts/status.sh
```
## Config Server
В локальном сценарии `spring-cloud-config-server` запускается с:
```text
SPRING_PROFILES_ACTIVE=local
CONFIG_NATIVE_SEARCH_LOCATIONS=file:/config-repo
```
`CONFIG_GIT_URI`, GitLab token и `git clone` не используются.
Сервисы запускаются с профилями:
```text
SPRING_PROFILES_ACTIVE=local,docker-local
SPRING_CLOUD_CONFIG_PROFILE=local,docker-local
SPRING_CLOUD_CONFIG_LABEL=
```
Профиль `local` сохраняет существующие service-specific настройки, а `docker-local` переопределяет IP-адреса на Docker Compose DNS names:
- `postgres`
- `kafka`
- `pcp-ballistics-service`
- `slots-service`
- остальные service names из `docker-compose.yml`
## Базы данных
`scripts/init-db.sh` создаёт только пустые базы данных:
- `pcp_ballistics`
- `pcp_requests`
- `pcp_route_processing`
- `pcp_missions`
- `pcp_slots`
- `pcp_complex_plan`
- `pcp_satellite_catalog`
- `pcp_stations`
- `pcp_tgu`
- `pcp_satellites`
Таблицы, схемы и seed data вручную не создаются. Схемы создаются существующими Flyway migrations внутри сервисов при старте приложений.
`pcp_satellites` добавлена дополнительно, потому что текущий `config-repo/pcp-complex-mission-service.yaml` использует эту БД для `pcp-complex-mission-service`.
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
version="${LOCAL_BUNDLE_VERSION:-${1:-}}"
if [[ -z "$version" ]]; then
if git -C "$repo_root" rev-parse --short HEAD >/dev/null 2>&1; then
version="$(git -C "$repo_root" rev-parse --short HEAD)"
else
version="$(date +%Y%m%d%H%M%S)"
fi
fi
bundle_name="pcp-local-bundle-$version"
build_dir="$script_dir/build"
dist_dir="$build_dir/$bundle_name"
docker_contexts_dir="$build_dir/docker-contexts"
archive_path="$build_dir/$bundle_name.tar.gz"
service_names=(
spring-cloud-config-server
pcp-srpring-boot-admin-server
pcp-satellite-catalog-service
pcp-ballistics-service
pcp-request-service
pcp-route-processing-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
pcp-coverage-scheme-service
pcp-dynamic-plan-service
pcp-stations-service
pcp-ui-service
pcp-tgu-service
)
gradle_max_workers="${GRADLE_MAX_WORKERS:-1}"
gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}"
kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}"
gradle_offline="${GRADLE_OFFLINE:-true}"
runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}"
include_infra_images="${INCLUDE_INFRA_IMAGES:-false}"
gradle_common_args=(
"--no-parallel"
"--max-workers=$gradle_max_workers"
"-Dorg.gradle.jvmargs=$gradle_jvmargs"
"-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs"
)
if [[ "$gradle_offline" == "true" ]]; then
gradle_common_args+=("--offline")
fi
infra_images=(
"${POSTGRES_IMAGE:-postgis/postgis:16-3.4}"
"${KAFKA_IMAGE:-apache/kafka:3.7.2}"
)
sanitize_image_name() {
printf '%s' "$1" | sed 's#[/:@]#-#g'
}
copy_single_boot_jar() {
local service_name="$1"
local context_dir="$2"
local jar_count
local jar_file
jar_count="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l
)"
if [[ "$jar_count" != "1" ]]; then
echo "Expected exactly one bootJar for $service_name, found $jar_count:" >&2
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' -print >&2 || true
exit 1
fi
jar_file="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1
)"
mkdir -p "$context_dir/build/libs"
cp "$repo_root/services/$service_name/Dockerfile" "$context_dir/Dockerfile"
cp "$jar_file" "$context_dir/build/libs/app.jar"
}
copy_config_repo() {
local target_dir="$1"
local source_file
mkdir -p "$target_dir"
(
cd "$repo_root/config-repo"
find . -type f \
! -path './build/*' \
! -path './.gradle/*' \
! -name '*.log' \
! -name '*.tmp' \
-print0
) | while IFS= read -r -d '' source_file; do
mkdir -p "$target_dir/$(dirname "$source_file")"
cp "$repo_root/config-repo/$source_file" "$target_dir/$source_file"
done
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "$command_name is required." >&2
exit 1
fi
}
require_command docker
require_command tar
echo "Building bootJar artifacts..."
echo "Gradle max workers: $gradle_max_workers"
echo "Gradle JVM args: $gradle_jvmargs"
echo "Kotlin daemon JVM args: $kotlin_daemon_jvmargs"
echo "Gradle offline mode: $gradle_offline"
echo "Local bundle runtime image: $runtime_image"
for service_name in "${service_names[@]}"; do
echo "Building bootJar for $service_name..."
"$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
done
rm -rf "$dist_dir" "$docker_contexts_dir" "$archive_path"
mkdir -p "$dist_dir/images" "$docker_contexts_dir"
echo "Building service Docker images..."
service_images=()
for service_name in "${service_names[@]}"; do
image_name="pcp/$service_name:$version"
context_dir="$docker_contexts_dir/$service_name"
copy_single_boot_jar "$service_name" "$context_dir"
docker build \
--build-arg "RUNTIME_IMAGE=$runtime_image" \
--file "$context_dir/Dockerfile" \
--tag "$image_name" \
"$context_dir"
service_images+=("$image_name")
done
echo "Saving service Docker images into one archive..."
docker save --output "$dist_dir/images/pcp-services.tar" "${service_images[@]}"
if [[ "$include_infra_images" == "true" ]]; then
echo "Pulling and saving infrastructure images..."
for image_name in "${infra_images[@]}"; do
image_file="$(sanitize_image_name "$image_name").tar"
docker pull "$image_name"
docker save --output "$dist_dir/images/$image_file" "$image_name"
done
else
echo "Skipping public infrastructure images. They will be pulled by docker compose on the target machine."
printf '%s\n' "${infra_images[@]}" > "$dist_dir/images/public-images.txt"
fi
echo "Preparing bundle files..."
cp "$script_dir/docker-compose.yml" "$dist_dir/docker-compose.yml"
cp "$script_dir/.env.example" "$dist_dir/.env.example"
cp "$script_dir/.env.example" "$dist_dir/.env"
sed -i "s/^PCP_IMAGE_TAG=.*/PCP_IMAGE_TAG=$version/" "$dist_dir/.env"
cp "$script_dir/README-local.md" "$dist_dir/README-local.md"
cp -R "$script_dir/scripts" "$dist_dir/scripts"
copy_config_repo "$dist_dir/config-repo"
find "$dist_dir/scripts" -type f -name '*.sh' -exec chmod +x {} +
echo "Creating archive..."
tar -C "$build_dir" -czf "$archive_path" "$bundle_name"
echo "Local bundle created: $archive_path"
echo "Bundle version: $version"
echo "To use: tar -xzf $archive_path && cd $bundle_name"
+205
View File
@@ -0,0 +1,205 @@
name: ${COMPOSE_PROJECT_NAME:-pcp-local}
x-pcp-service-env: &pcp-service-env
CONFIG_SERVER_URI: http://spring-cloud-config-server:8888
CONFIG_SERVER_FAIL_FAST: "true"
SPRING_PROFILES_ACTIVE: local,docker-local
SPRING_CLOUD_CONFIG_PROFILE: local,docker-local
SPRING_CLOUD_CONFIG_LABEL: ""
SPRING_KAFKA_ADMIN_AUTO_CREATE: "true"
SPRING_KAFKA_LISTENER_AUTO_STARTUP: "true"
PCP_POSTGRES_HOST: ${PCP_POSTGRES_HOST:-postgres}
PCP_POSTGRES_PORT: ${PCP_POSTGRES_PORT:-5432}
PCP_KAFKA_HOST: ${PCP_KAFKA_HOST:-kafka}
PCP_KAFKA_PORT: ${PCP_KAFKA_PORT:-9092}
CAMUNDA_CLIENT_ENABLED: ${CAMUNDA_CLIENT_ENABLED:-false}
CAMUNDA_GRPC_ADDRESS: ${CAMUNDA_GRPC_ADDRESS:-http://host.docker.internal:26500}
x-pcp-service-common: &pcp-service-common
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
spring-cloud-config-server:
condition: service_started
services:
postgres:
image: ${POSTGRES_IMAGE:-postgis/postgis:16-3.4}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
POSTGRES_DB: postgres
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d postgres"]
interval: 5s
timeout: 5s
retries: 30
kafka:
image: ${KAFKA_IMAGE:-apache/kafka:3.7.2}
restart: unless-stopped
environment:
KAFKA_NODE_ID: "1"
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:29092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,EXTERNAL://localhost:${KAFKA_PORT:-29092}
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: "1"
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: "1"
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: "1"
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: "0"
KAFKA_LOG_DIRS: /var/lib/kafka/data
ports:
- "${KAFKA_PORT:-29092}:29092"
volumes:
- kafka-data:/var/lib/kafka/data
healthcheck:
test: ["CMD-SHELL", "kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1"]
interval: 10s
timeout: 10s
retries: 30
spring-cloud-config-server:
image: pcp/spring-cloud-config-server:${PCP_IMAGE_TAG:-local}
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
SPRING_PROFILES_ACTIVE: local
CONFIG_NATIVE_SEARCH_LOCATIONS: file:/config-repo
SERVER_PORT: 8888
ports:
- "${CONFIG_SERVER_PORT:-38888}:8888"
volumes:
- ${CONFIG_REPO_DIR:-./config-repo}:/config-repo:ro
pcp-srpring-boot-admin-server:
<<: *pcp-service-common
image: pcp/pcp-srpring-boot-admin-server:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_ADMIN_PORT:-7000}:7000"
pcp-complex-mission-service:
<<: *pcp-service-common
image: pcp/pcp-complex-mission-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_COMPLEX_MISSION_PORT:-7002}:7002"
pcp-ballistics-service:
<<: *pcp-service-common
image: pcp/pcp-ballistics-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_BALLISTICS_PORT:-7003}:7003"
pcp-request-service:
<<: *pcp-service-common
image: pcp/pcp-request-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_REQUEST_PORT:-7005}:7005"
pcp-route-processing-service:
<<: *pcp-service-common
image: pcp/pcp-route-processing-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_ROUTE_PROCESSING_PORT:-7004}:7004"
slots-service:
<<: *pcp-service-common
image: pcp/slots-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_SLOTS_PORT:-7006}:7006"
pcp-ui-service:
<<: *pcp-service-common
image: pcp/pcp-ui-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_UI_PORT:-7008}:7008"
pcp-stations-service:
<<: *pcp-service-common
image: pcp/pcp-stations-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_STATIONS_PORT:-7009}:7009"
pcp-mission-planing-service:
<<: *pcp-service-common
image: pcp/pcp-mission-planing-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_MISSION_PORT:-7010}:7010"
pcp-tgu-service:
<<: *pcp-service-common
image: pcp/pcp-tgu-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_TGU_PORT:-7011}:7011"
pcp-coverage-scheme-service:
<<: *pcp-service-common
image: pcp/pcp-coverage-scheme-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_COVERAGE_SCHEME_PORT:-7012}:7012"
pcp-satellite-catalog-service:
<<: *pcp-service-common
image: pcp/pcp-satellite-catalog-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_SATELLITE_CATALOG_PORT:-7013}:7013"
pcp-dynamic-plan-service:
<<: *pcp-service-common
image: pcp/pcp-dynamic-plan-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_DYNAMIC_PLAN_PORT:-7014}:7014"
pcp-gateway-service:
<<: *pcp-service-common
image: pcp/pcp-gateway-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_GATEWAY_PORT:-7080}:7080"
volumes:
postgres-data:
kafka-data:
networks:
default:
name: ${PCP_DOCKER_NETWORK:-pcp-local_default}
external: ${PCP_DOCKER_NETWORK_EXTERNAL:-false}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
env_file="$script_dir/.env"
env_example_file="$script_dir/.env.example"
docker_contexts_dir="$script_dir/build/local-docker-contexts"
service_names=(
spring-cloud-config-server
pcp-srpring-boot-admin-server
pcp-satellite-catalog-service
pcp-ballistics-service
pcp-request-service
pcp-route-processing-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
pcp-coverage-scheme-service
pcp-dynamic-plan-service
pcp-stations-service
pcp-ui-service
pcp-tgu-service
)
ensure_env_file() {
if [[ ! -f "$env_file" && -f "$env_example_file" ]]; then
cp "$env_example_file" "$env_file"
echo "Created $env_file from .env.example"
fi
}
ensure_source_config_repo_dir() {
if grep -q '^CONFIG_REPO_DIR=' "$env_file"; then
sed -i "s#^CONFIG_REPO_DIR=.*#CONFIG_REPO_DIR=$repo_root/config-repo#" "$env_file"
else
printf '\nCONFIG_REPO_DIR=%s/config-repo\n' "$repo_root" >> "$env_file"
fi
}
load_env() {
set -a
# shellcheck disable=SC1090
source "$env_file"
set +a
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "$command_name is required." >&2
exit 1
fi
}
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
copy_single_boot_jar() {
local service_name="$1"
local context_dir="$2"
local jar_count
local jar_file
jar_count="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l
)"
if [[ "$jar_count" != "1" ]]; then
echo "Expected exactly one bootJar for $service_name, found $jar_count:" >&2
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' -print >&2 || true
exit 1
fi
jar_file="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1
)"
mkdir -p "$context_dir/build/libs"
cp "$repo_root/services/$service_name/Dockerfile" "$context_dir/Dockerfile"
cp "$jar_file" "$context_dir/build/libs/app.jar"
}
build_images() {
local gradle_max_workers="${GRADLE_MAX_WORKERS:-1}"
local gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}"
local kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}"
local gradle_offline="${GRADLE_OFFLINE:-true}"
local runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}"
local image_tag="${PCP_IMAGE_TAG:-local}"
local service_name
local image_name
local context_dir
local gradle_common_args=(
"--no-parallel"
"--max-workers=$gradle_max_workers"
"-Dorg.gradle.jvmargs=$gradle_jvmargs"
"-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs"
)
if [[ "$gradle_offline" == "true" ]]; then
gradle_common_args+=("--offline")
fi
echo "Building local Docker images..."
echo "Gradle offline mode: $gradle_offline"
echo "Local image tag: $image_tag"
echo "Runtime image: $runtime_image"
for service_name in "${service_names[@]}"; do
echo "Building bootJar for $service_name..."
"$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
done
rm -rf "$docker_contexts_dir"
mkdir -p "$docker_contexts_dir"
for service_name in "${service_names[@]}"; do
image_name="pcp/$service_name:$image_tag"
context_dir="$docker_contexts_dir/$service_name"
copy_single_boot_jar "$service_name" "$context_dir"
echo "Building Docker image $image_name..."
docker build \
--build-arg "RUNTIME_IMAGE=$runtime_image" \
--file "$context_dir/Dockerfile" \
--tag "$image_name" \
"$context_dir"
done
}
usage() {
cat <<'EOF'
Usage:
./deploy/local-bundle/run-local.sh [command]
Commands:
start Build service images locally and start docker compose.
restart Stop compose stack, then build and start.
build Build service images locally, without starting compose.
stop Stop compose stack.
status Show compose status.
logs Follow compose logs.
EOF
}
start_local() {
build_images
"$script_dir/scripts/start.sh"
}
command="${1:-start}"
case "$command" in
help|-h|--help)
usage
exit 0
;;
esac
ensure_env_file
ensure_source_config_repo_dir
load_env
require_command docker
case "$command" in
start)
start_local
;;
restart)
"$script_dir/scripts/stop.sh"
start_local
;;
build)
build_images
;;
stop)
"$script_dir/scripts/stop.sh"
;;
status)
"$script_dir/scripts/status.sh"
;;
logs)
compose logs -f
;;
*)
usage >&2
exit 1
;;
esac
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
env_file="$script_dir/.env"
env_example_file="$script_dir/.env.example"
docker_contexts_dir="$script_dir/build/local-docker-contexts"
service_names=(
spring-cloud-config-server
pcp-srpring-boot-admin-server
pcp-satellite-catalog-service
pcp-ballistics-service
pcp-request-service
pcp-route-processing-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
pcp-coverage-scheme-service
pcp-dynamic-plan-service
pcp-stations-service
pcp-ui-service
pcp-tgu-service
)
usage() {
cat <<'EOF'
Usage:
./deploy/local-bundle/run-service.sh SERVICE [command]
Commands:
restart Build SERVICE bootJar, rebuild its Docker image and recreate only this compose service.
build Build SERVICE bootJar and Docker image, without restarting container.
stop Stop SERVICE container.
logs Follow SERVICE logs.
Examples:
./deploy/local-bundle/run-service.sh pcp-request-service
./deploy/local-bundle/run-service.sh pcp-request-service build
EOF
}
ensure_env_file() {
if [[ ! -f "$env_file" && -f "$env_example_file" ]]; then
cp "$env_example_file" "$env_file"
echo "Created $env_file from .env.example"
fi
}
ensure_source_config_repo_dir() {
if grep -q '^CONFIG_REPO_DIR=' "$env_file"; then
sed -i "s#^CONFIG_REPO_DIR=.*#CONFIG_REPO_DIR=$repo_root/config-repo#" "$env_file"
else
printf '\nCONFIG_REPO_DIR=%s/config-repo\n' "$repo_root" >> "$env_file"
fi
}
load_env() {
set -a
# shellcheck disable=SC1090
source "$env_file"
set +a
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "$command_name is required." >&2
exit 1
fi
}
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
is_known_service() {
local service_name="$1"
local known_service_name
for known_service_name in "${service_names[@]}"; do
if [[ "$known_service_name" == "$service_name" ]]; then
return 0
fi
done
return 1
}
copy_single_boot_jar() {
local service_name="$1"
local context_dir="$2"
local jar_count
local jar_file
jar_count="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l
)"
if [[ "$jar_count" != "1" ]]; then
echo "Expected exactly one bootJar for $service_name, found $jar_count:" >&2
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' -print >&2 || true
exit 1
fi
jar_file="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1
)"
mkdir -p "$context_dir/build/libs"
cp "$repo_root/services/$service_name/Dockerfile" "$context_dir/Dockerfile"
cp "$jar_file" "$context_dir/build/libs/app.jar"
}
build_service_image() {
local service_name="$1"
local gradle_max_workers="${GRADLE_MAX_WORKERS:-1}"
local gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}"
local kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}"
local gradle_offline="${GRADLE_OFFLINE:-true}"
local runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}"
local image_tag="${PCP_IMAGE_TAG:-local}"
local image_name="pcp/$service_name:$image_tag"
local context_dir="$docker_contexts_dir/$service_name"
local gradle_common_args=(
"--no-parallel"
"--max-workers=$gradle_max_workers"
"-Dorg.gradle.jvmargs=$gradle_jvmargs"
"-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs"
)
if [[ "$gradle_offline" == "true" ]]; then
gradle_common_args+=("--offline")
fi
echo "Building bootJar for $service_name..."
"$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
rm -rf "$context_dir"
copy_single_boot_jar "$service_name" "$context_dir"
echo "Building Docker image $image_name..."
docker build \
--build-arg "RUNTIME_IMAGE=$runtime_image" \
--file "$context_dir/Dockerfile" \
--tag "$image_name" \
"$context_dir"
}
http_get() {
local url="$1"
if command -v curl >/dev/null 2>&1; then
curl -fsS "$url" >/dev/null
elif command -v wget >/dev/null 2>&1; then
wget -q -O /dev/null "$url"
else
return 2
fi
}
wait_for_config_server() {
local attempt
local url="http://localhost:${CONFIG_SERVER_PORT:-38888}/actuator/health"
for attempt in {1..90}; do
if http_get "$url"; then
echo "spring-cloud-config-server is ready: $url"
return 0
fi
sleep 2
done
echo "spring-cloud-config-server is not ready after 180 seconds: $url" >&2
exit 1
}
restart_service() {
local service_name="$1"
build_service_image "$service_name"
if [[ "$service_name" == "spring-cloud-config-server" ]]; then
compose up -d --no-deps --force-recreate "$service_name"
wait_for_config_server
return 0
fi
compose up -d spring-cloud-config-server
wait_for_config_server
compose up -d --no-deps --force-recreate "$service_name"
}
service_name="${1:-}"
command="${2:-restart}"
if [[ -z "$service_name" || "$service_name" == "help" || "$service_name" == "-h" || "$service_name" == "--help" ]]; then
usage
exit 0
fi
if ! is_known_service "$service_name"; then
echo "Unknown service: $service_name" >&2
echo "Known services:" >&2
printf ' %s\n' "${service_names[@]}" >&2
exit 1
fi
ensure_env_file
ensure_source_config_repo_dir
load_env
require_command docker
case "$command" in
restart)
restart_service "$service_name"
;;
build)
build_service_image "$service_name"
;;
stop)
compose stop "$service_name"
;;
logs)
compose logs -f "$service_name"
;;
help|-h|--help)
usage
;;
*)
usage >&2
exit 1
;;
esac
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bundle_dir="$(cd "$script_dir/.." && pwd)"
images_dir="$bundle_dir/images"
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
ensure_env_file() {
if [[ ! -f "$bundle_dir/.env" && -f "$bundle_dir/.env.example" ]]; then
cp "$bundle_dir/.env.example" "$bundle_dir/.env"
echo "Created .env from .env.example"
fi
}
ensure_env_file
set -a
# shellcheck disable=SC1091
source "$bundle_dir/.env"
set +a
if [[ ! -d "$images_dir" ]]; then
echo "Images directory not found: $images_dir" >&2
exit 1
fi
shopt -s nullglob
image_archives=("$images_dir"/*.tar)
if (( ${#image_archives[@]} == 0 )); then
echo "No Docker image archives found in $images_dir" >&2
exit 1
fi
for image_archive in "${image_archives[@]}"; do
echo "Loading image: $(basename "$image_archive")"
docker load -i "$image_archive"
done
if [[ -f "$images_dir/public-images.txt" ]]; then
echo "Public images are not bundled and will be pulled by docker compose if missing:"
sed 's/^/ - /' "$images_dir/public-images.txt"
fi
cd "$bundle_dir"
if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then
echo "Using external infrastructure. Starting spring-cloud-config-server only."
compose up -d spring-cloud-config-server
else
echo "Starting postgres, kafka and spring-cloud-config-server..."
compose up -d postgres kafka spring-cloud-config-server
fi
echo "Images imported and base services requested."
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bundle_dir="$(cd "$script_dir/.." && pwd)"
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
ensure_env_file() {
if [[ ! -f "$bundle_dir/.env" && -f "$bundle_dir/.env.example" ]]; then
cp "$bundle_dir/.env.example" "$bundle_dir/.env"
echo "Created .env from .env.example"
fi
}
load_env() {
set -a
# shellcheck disable=SC1091
source "$bundle_dir/.env"
set +a
}
wait_for_postgres() {
local attempt
for attempt in {1..60}; do
if compose exec -T postgres pg_isready -U "${POSTGRES_USER:-postgres}" -d postgres >/dev/null 2>&1; then
return 0
fi
sleep 2
done
echo "Postgres is not ready after 120 seconds." >&2
exit 1
}
wait_for_external_postgres() {
local container_name="${EXTERNAL_POSTGRES_CONTAINER:-}"
local attempt
if [[ -z "$container_name" ]]; then
echo "EXTERNAL_POSTGRES_CONTAINER is required when USE_EXTERNAL_INFRA=true and SKIP_DB_INIT is not true." >&2
exit 1
fi
for attempt in {1..60}; do
if docker exec "$container_name" pg_isready -U "${POSTGRES_USER:-postgres}" -d postgres >/dev/null 2>&1; then
return 0
fi
sleep 2
done
echo "External Postgres is not ready after 120 seconds: $container_name" >&2
exit 1
}
create_database_if_missing() {
local db_name="$1"
local exists
exists="$(
compose exec -T postgres psql -U "${POSTGRES_USER:-postgres}" -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname = '$db_name'" | tr -d '[:space:]'
)"
if [[ "$exists" == "1" ]]; then
echo "Database already exists: $db_name"
return 0
fi
echo "Creating database: $db_name"
compose exec -T postgres createdb -U "${POSTGRES_USER:-postgres}" "$db_name"
}
create_external_database_if_missing() {
local db_name="$1"
local container_name="${EXTERNAL_POSTGRES_CONTAINER:-}"
local exists
exists="$(
docker exec "$container_name" psql -U "${POSTGRES_USER:-postgres}" -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname = '$db_name'" | tr -d '[:space:]'
)"
if [[ "$exists" == "1" ]]; then
echo "Database already exists: $db_name"
return 0
fi
echo "Creating database: $db_name"
docker exec "$container_name" createdb -U "${POSTGRES_USER:-postgres}" "$db_name"
}
databases=(
pcp_ballistics
pcp_requests
pcp_route_processing
pcp_missions
pcp_slots
pcp_complex_plan
pcp_satellite_catalog
pcp_stations
pcp_tgu
pcp_satellites
)
ensure_env_file
load_env
cd "$bundle_dir"
if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then
wait_for_external_postgres
for db_name in "${databases[@]}"; do
create_external_database_if_missing "$db_name"
done
else
compose up -d postgres
wait_for_postgres
for db_name in "${databases[@]}"; do
create_database_if_missing "$db_name"
done
fi
echo "Database initialization completed. Tables are left to service Flyway migrations."
echo "No schema objects or seed data were created by this script."
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bundle_dir="$(cd "$script_dir/.." && pwd)"
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
ensure_env_file() {
if [[ ! -f "$bundle_dir/.env" && -f "$bundle_dir/.env.example" ]]; then
cp "$bundle_dir/.env.example" "$bundle_dir/.env"
echo "Created .env from .env.example"
fi
}
load_env() {
set -a
# shellcheck disable=SC1091
source "$bundle_dir/.env"
set +a
}
http_get() {
local url="$1"
if command -v curl >/dev/null 2>&1; then
curl -fsS "$url" >/dev/null
elif command -v wget >/dev/null 2>&1; then
wget -q -O /dev/null "$url"
else
return 2
fi
}
wait_for_http() {
local name="$1"
local url="$2"
local attempt
for attempt in {1..90}; do
if http_get "$url"; then
echo "$name is ready: $url"
return 0
fi
sleep 2
done
echo "$name is not ready after 180 seconds: $url" >&2
exit 1
}
wait_for_kafka() {
local attempt
for attempt in {1..60}; do
if compose exec -T kafka kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1; then
echo "Kafka is ready."
return 0
fi
sleep 2
done
echo "Kafka is not ready after 120 seconds." >&2
exit 1
}
wait_for_external_kafka() {
local container_name="${EXTERNAL_KAFKA_CONTAINER:-}"
local attempt
if [[ -z "$container_name" ]]; then
echo "External Kafka container is not configured; skipping Kafka readiness check."
return 0
fi
for attempt in {1..60}; do
if docker exec "$container_name" kafka-topics --bootstrap-server "${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}" --list >/dev/null 2>&1 ||
docker exec "$container_name" kafka-topics.sh --bootstrap-server "${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}" --list >/dev/null 2>&1; then
echo "External Kafka is ready in container $container_name."
return 0
fi
sleep 2
done
echo "External Kafka is not ready after 120 seconds: $container_name" >&2
exit 1
}
app_services=(
pcp-srpring-boot-admin-server
pcp-satellite-catalog-service
pcp-ballistics-service
pcp-request-service
pcp-route-processing-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
pcp-coverage-scheme-service
pcp-dynamic-plan-service
pcp-stations-service
pcp-ui-service
pcp-tgu-service
)
ensure_env_file
load_env
cd "$bundle_dir"
if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then
echo "Using external infrastructure. Skipping local postgres/kafka startup."
echo "Postgres endpoint for services: ${PCP_POSTGRES_HOST:-postgres}:${PCP_POSTGRES_PORT:-5432}"
echo "Kafka endpoint for services: ${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}"
wait_for_external_kafka
else
echo "Starting infrastructure..."
compose up -d postgres kafka
wait_for_kafka
fi
if [[ "${SKIP_DB_INIT:-false}" != "true" ]]; then
"$script_dir/init-db.sh"
fi
echo "Starting spring-cloud-config-server..."
compose up -d spring-cloud-config-server
wait_for_http "spring-cloud-config-server" "http://localhost:${CONFIG_SERVER_PORT:-38888}/actuator/health"
echo "Starting application services..."
for service_name in "${app_services[@]}"; do
echo "Starting $service_name..."
compose up -d "$service_name"
done
echo "Application startup requested."
compose ps
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bundle_dir="$(cd "$script_dir/.." && pwd)"
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
cd "$bundle_dir"
compose ps
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bundle_dir="$(cd "$script_dir/.." && pwd)"
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
cd "$bundle_dir"
compose down "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-ballistics-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-complex-mission-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-coverage-scheme-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-dynamic-plan-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-mission-planing-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-request-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-route-processing-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-satellite-catalog-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-srpring-boot-admin-server "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-stations-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-tgu-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-ui-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" slots-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" spring-cloud-config-server "$@"
+79
View File
@@ -0,0 +1,79 @@
# Local jar runner
Скрипт запускает backend-сервисы как Spring Boot jar'ы на локальной машине.
Spring Cloud Config Server стартует первым и читает конфигурацию из локального
`config-repo`.
## Запуск
Из корня репозитория:
```bash
./deploy/local-jar/run-services.sh start
```
Что делает `start`:
- собирает `bootJar` для `spring-cloud-config-server` и всех Spring Boot сервисов;
- поднимает локальные `postgres` и `kafka` через `deploy/local-bundle/docker-compose.yml`;
- создаёт сервисные базы данных, если их ещё нет;
- запускает `spring-cloud-config-server` на `http://localhost:38888`;
- проверяет lookup конфигурации из `config-repo`;
- запускает остальные jar'ы в заданной последовательности.
Runtime-файлы создаются в `build/local-jar-runtime`:
- `pids/` - PID процессов;
- `logs/` - stdout/stderr каждого сервиса.
## Команды
```bash
./deploy/local-jar/run-services.sh status
./deploy/local-jar/run-services.sh logs pcp-request-service
./deploy/local-jar/run-services.sh stop
./deploy/local-jar/run-services.sh restart
```
Только сборка:
```bash
./deploy/local-jar/run-services.sh build
```
Только инфраструктура:
```bash
./deploy/local-jar/run-services.sh infra-up
./deploy/local-jar/run-services.sh infra-down
```
## Настройки
По умолчанию config-server использует:
```text
SPRING_PROFILES_ACTIVE=local
CONFIG_NATIVE_SEARCH_LOCATIONS=file:<repo>/config-repo
SERVER_PORT=38888
```
Сервисы используют:
```text
SPRING_PROFILES_ACTIVE=local
SPRING_CLOUD_CONFIG_PROFILE=local
SPRING_CLOUD_CONFIG_LABEL=
CONFIG_SERVER_URI=http://localhost:38888
```
Полезные override'ы:
```bash
START_INFRA=false ./deploy/local-jar/run-services.sh start
INIT_DB=false ./deploy/local-jar/run-services.sh start
WAIT_FOR_APPS=false ./deploy/local-jar/run-services.sh start
JAVA_OPTS="-Xmx1g" ./deploy/local-jar/run-services.sh start
CONFIG_REPO_DIR=/path/to/config-repo ./deploy/local-jar/run-services.sh start
LOCAL_SERVICE_PROFILES=local,foo ./deploy/local-jar/run-services.sh start
```
+538
View File
@@ -0,0 +1,538 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
local_bundle_dir="$repo_root/deploy/local-bundle"
compose_file="$local_bundle_dir/docker-compose.yml"
local_bundle_env_file="$local_bundle_dir/.env"
if [[ -f "$local_bundle_env_file" ]]; then
set -a
# shellcheck disable=SC1090
source "$local_bundle_env_file"
set +a
fi
config_repo_dir="${CONFIG_REPO_DIR:-$repo_root/config-repo}"
runtime_dir="${LOCAL_JAR_RUNTIME_DIR:-$repo_root/build/local-jar-runtime}"
pid_dir="$runtime_dir/pids"
log_dir="$runtime_dir/logs"
if [[ "$config_repo_dir" != /* ]]; then
config_repo_dir="$repo_root/$config_repo_dir"
fi
if [[ "$runtime_dir" != /* ]]; then
runtime_dir="$repo_root/$runtime_dir"
pid_dir="$runtime_dir/pids"
log_dir="$runtime_dir/logs"
fi
config_service="spring-cloud-config-server"
app_services=(
pcp-srpring-boot-admin-server
pcp-stations-service
pcp-satellite-catalog-service
pcp-request-service
pcp-route-processing-service
pcp-ballistics-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
pcp-coverage-scheme-service
pcp-dynamic-plan-service
tle-monitoring-service
pcp-tgu-service
pcp-ui-service
)
all_services=("$config_service" "${app_services[@]}")
databases=(
pcp_ballistics
pcp_requests
pcp_route_processing
pcp_missions
pcp_slots
pcp_complex_plan
pcp_satellite_catalog
pcp_stations
pcp_tgu
pcp_tle
pcp_satellites
)
declare -A service_ports=(
[spring-cloud-config-server]="${CONFIG_SERVER_PORT:-38888}"
[pcp-srpring-boot-admin-server]="${PCP_ADMIN_PORT:-7000}"
[tle-monitoring-service]="${PCP_TLE_MONITORING_PORT:-7001}"
[pcp-complex-mission-service]="${PCP_COMPLEX_MISSION_PORT:-7002}"
[pcp-ballistics-service]="${PCP_BALLISTICS_PORT:-7003}"
[pcp-route-processing-service]="${PCP_ROUTE_PROCESSING_PORT:-7004}"
[pcp-request-service]="${PCP_REQUEST_PORT:-7005}"
[slots-service]="${PCP_SLOTS_PORT:-7006}"
[pcp-ui-service]="${PCP_UI_PORT:-7008}"
[pcp-stations-service]="${PCP_STATIONS_PORT:-7009}"
[pcp-mission-planing-service]="${PCP_MISSION_PORT:-7010}"
[pcp-tgu-service]="${PCP_TGU_PORT:-7011}"
[pcp-coverage-scheme-service]="${PCP_COVERAGE_SCHEME_PORT:-7012}"
[pcp-satellite-catalog-service]="${PCP_SATELLITE_CATALOG_PORT:-7013}"
[pcp-dynamic-plan-service]="${PCP_DYNAMIC_PLAN_PORT:-7014}"
)
gradle_max_workers="${GRADLE_MAX_WORKERS:-1}"
gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}"
kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}"
config_server_port="${service_ports[$config_service]}"
config_server_uri="${LOCAL_CONFIG_SERVER_URI:-http://localhost:$config_server_port}"
service_profiles="${LOCAL_SERVICE_PROFILES:-local}"
config_profiles="${LOCAL_CONFIG_PROFILES:-$service_profiles}"
postgres_host="${PCP_POSTGRES_HOST:-localhost}"
postgres_port="${PCP_POSTGRES_PORT:-5432}"
kafka_host="${PCP_KAFKA_HOST:-localhost}"
kafka_port="${PCP_KAFKA_PORT:-29092}"
gradle_common_args=(
"--no-parallel"
"--max-workers=$gradle_max_workers"
"-Dorg.gradle.jvmargs=$gradle_jvmargs"
"-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs"
)
usage() {
cat <<'EOF'
Usage:
./deploy/local-jar/run-services.sh [command]
Commands:
start Build bootJar artifacts and start local infra, config-server and services.
restart Stop local jar processes, then run start.
stop Stop jar processes started by this script.
status Show pid and log status for local jar processes.
build Build bootJar artifacts only.
infra-up Start local Postgres/Kafka via deploy/local-bundle/docker-compose.yml.
infra-down Stop local Postgres/Kafka containers.
logs NAME Tail log for a service.
Useful environment:
START_INFRA=false Do not start Postgres/Kafka containers during start.
INIT_DB=false Do not create local Postgres databases during start.
WAIT_FOR_APPS=false Do not wait for each app /actuator/health.
CONFIG_REPO_DIR=... Config repository used by spring-cloud-config-server.
JAVA_OPTS="-Xmx1g" Extra JVM options for every jar process.
LOCAL_SERVICE_PROFILES=local,foo
EOF
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "$command_name is required." >&2
exit 1
fi
}
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose --project-directory "$local_bundle_dir" -f "$compose_file" "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose --project-directory "$local_bundle_dir" -f "$compose_file" "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
http_get() {
local url="$1"
if command -v curl >/dev/null 2>&1; then
curl -fsS "$url" >/dev/null
elif command -v wget >/dev/null 2>&1; then
wget -q -O /dev/null "$url"
else
return 2
fi
}
wait_for_http() {
local name="$1"
local url="$2"
local attempts="${3:-90}"
local attempt
for ((attempt = 1; attempt <= attempts; attempt++)); do
if http_get "$url"; then
echo "$name is ready: $url"
return 0
fi
sleep 2
done
echo "$name is not ready after $((attempts * 2)) seconds: $url" >&2
exit 1
}
wait_for_postgres() {
local attempt
local postgres_user="${POSTGRES_USER:-postgres}"
for attempt in {1..60}; do
if compose exec -T postgres pg_isready -U "$postgres_user" -d postgres >/dev/null 2>&1; then
echo "Postgres is ready."
return 0
fi
sleep 2
done
echo "Postgres is not ready after 120 seconds." >&2
exit 1
}
wait_for_kafka() {
local attempt
for attempt in {1..60}; do
if compose exec -T kafka kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1; then
echo "Kafka is ready."
return 0
fi
sleep 2
done
echo "Kafka is not ready after 120 seconds." >&2
exit 1
}
create_database_if_missing() {
local db_name="$1"
local postgres_user="${POSTGRES_USER:-postgres}"
local exists
exists="$(
compose exec -T postgres psql -U "$postgres_user" -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname = '$db_name'" | tr -d '[:space:]'
)"
if [[ "$exists" == "1" ]]; then
echo "Database already exists: $db_name"
return 0
fi
echo "Creating database: $db_name"
compose exec -T postgres createdb -U "$postgres_user" "$db_name"
}
start_infra() {
require_command docker
echo "Starting local Postgres and Kafka..."
compose up -d postgres kafka
wait_for_postgres
wait_for_kafka
}
init_databases() {
echo "Initializing local service databases..."
for db_name in "${databases[@]}"; do
create_database_if_missing "$db_name"
done
}
build_services() {
echo "Building bootJar artifacts..."
echo "Gradle max workers: $gradle_max_workers"
echo "Gradle JVM args: $gradle_jvmargs"
echo "Kotlin daemon JVM args: $kotlin_daemon_jvmargs"
for service_name in "${all_services[@]}"; do
echo "Building bootJar for $service_name..."
"$repo_root/gradlew" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
done
}
service_pid_file() {
local service_name="$1"
printf '%s/%s.pid' "$pid_dir" "$service_name"
}
service_log_file() {
local service_name="$1"
printf '%s/%s.log' "$log_dir" "$service_name"
}
is_pid_running() {
local pid="$1"
[[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1
}
read_service_pid() {
local service_name="$1"
local pid_file
pid_file="$(service_pid_file "$service_name")"
if [[ -f "$pid_file" ]]; then
tr -d '[:space:]' < "$pid_file"
fi
}
find_boot_jar() {
local service_name="$1"
local jar_dir="$repo_root/services/$service_name/build/libs"
local jar_count
local jar_file
if [[ ! -d "$jar_dir" ]]; then
echo "Missing build/libs for $service_name. Run build first." >&2
exit 1
fi
jar_count="$(find "$jar_dir" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [[ "$jar_count" != "1" ]]; then
echo "Expected exactly one bootJar for $service_name, found $jar_count:" >&2
find "$jar_dir" -maxdepth 1 -type f -name '*.jar' -print >&2 || true
exit 1
fi
jar_file="$(find "$jar_dir" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
printf '%s' "$jar_file"
}
start_service() {
local service_name="$1"
local jar_file
local pid_file
local log_file
local existing_pid
local port="${service_ports[$service_name]}"
local java_cmd=(java)
local parsed_java_opts=()
pid_file="$(service_pid_file "$service_name")"
log_file="$(service_log_file "$service_name")"
existing_pid="$(read_service_pid "$service_name" || true)"
if is_pid_running "$existing_pid"; then
echo "$service_name is already running with pid $existing_pid"
return 0
fi
mkdir -p "$pid_dir" "$log_dir"
jar_file="$(find_boot_jar "$service_name")"
if [[ -n "${JAVA_OPTS:-}" ]]; then
read -r -a parsed_java_opts <<< "$JAVA_OPTS"
java_cmd+=("${parsed_java_opts[@]}")
fi
java_cmd+=(-jar "$jar_file")
echo "Starting $service_name on port $port..."
if [[ "$service_name" == "$config_service" ]]; then
(
cd "$repo_root"
env \
SPRING_PROFILES_ACTIVE=local \
CONFIG_NATIVE_SEARCH_LOCATIONS="file:$config_repo_dir" \
SERVER_PORT="$config_server_port" \
"${java_cmd[@]}" > "$log_file" 2>&1 &
echo "$!" > "$pid_file"
)
else
(
cd "$repo_root"
env \
CONFIG_SERVER_URI="$config_server_uri" \
CONFIG_SERVER_FAIL_FAST=true \
SPRING_PROFILES_ACTIVE="$service_profiles" \
SPRING_CLOUD_CONFIG_PROFILE="$config_profiles" \
SPRING_CLOUD_CONFIG_LABEL= \
SPRING_KAFKA_ADMIN_AUTO_CREATE=true \
SPRING_KAFKA_LISTENER_MISSING_TOPICS_FATAL=false \
CAMUNDA_CLIENT_ENABLED=false \
PLANNING_EXTERNAL_UPDATE_ENABLED=false \
TLE_POLLING_ENABLED=false \
PCP_POSTGRES_HOST="$postgres_host" \
PCP_POSTGRES_PORT="$postgres_port" \
PCP_KAFKA_HOST="$kafka_host" \
PCP_KAFKA_PORT="$kafka_port" \
PCP_PORTS_CONFIG="$config_server_port" \
PCP_PORTS_ADMIN="${service_ports[pcp-srpring-boot-admin-server]}" \
PCP_PORTS_TLE_MONITORING="${service_ports[tle-monitoring-service]}" \
PCP_PORTS_COMPLEX_MISSION_SERVICE="${service_ports[pcp-complex-mission-service]}" \
PCP_PORTS_BALLISTICS="${service_ports[pcp-ballistics-service]}" \
PCP_PORTS_ROUTE_PROCESSING="${service_ports[pcp-route-processing-service]}" \
PCP_PORTS_REQUEST="${service_ports[pcp-request-service]}" \
PCP_PORTS_SLOTS="${service_ports[slots-service]}" \
PCP_PORTS_UI="${service_ports[pcp-ui-service]}" \
PCP_PORTS_STATIONS="${service_ports[pcp-stations-service]}" \
PCP_PORTS_MISSION="${service_ports[pcp-mission-planing-service]}" \
PCP_PORTS_TGU="${service_ports[pcp-tgu-service]}" \
PCP_PORTS_COVERAGE_SCHEME="${service_ports[pcp-coverage-scheme-service]}" \
PCP_PORTS_SATELLITE_CATALOG="${service_ports[pcp-satellite-catalog-service]}" \
PCP_PORTS_DYNAMIC_PLAN="${service_ports[pcp-dynamic-plan-service]}" \
"${java_cmd[@]}" > "$log_file" 2>&1 &
echo "$!" > "$pid_file"
)
fi
echo "$service_name log: $log_file"
}
start_config_server() {
if [[ ! -d "$config_repo_dir" ]]; then
echo "Config repo directory does not exist: $config_repo_dir" >&2
exit 1
fi
start_service "$config_service"
wait_for_http "$config_service" "$config_server_uri/actuator/health"
wait_for_http "$config_service config-repo lookup" "$config_server_uri/pcp-request-service/local"
}
wait_for_service_health() {
local service_name="$1"
local port="${service_ports[$service_name]}"
wait_for_http "$service_name" "http://localhost:$port/actuator/health" "${APP_HEALTH_ATTEMPTS:-90}"
}
start_app_services() {
for service_name in "${app_services[@]}"; do
start_service "$service_name"
if [[ "${WAIT_FOR_APPS:-true}" == "true" ]]; then
wait_for_service_health "$service_name"
fi
done
}
stop_service() {
local service_name="$1"
local pid_file
local pid
local attempt
pid_file="$(service_pid_file "$service_name")"
pid="$(read_service_pid "$service_name" || true)"
if ! is_pid_running "$pid"; then
rm -f "$pid_file"
echo "$service_name is not running."
return 0
fi
echo "Stopping $service_name with pid $pid..."
kill "$pid" >/dev/null 2>&1 || true
for attempt in {1..30}; do
if ! is_pid_running "$pid"; then
rm -f "$pid_file"
return 0
fi
sleep 1
done
echo "Force stopping $service_name with pid $pid..."
kill -9 "$pid" >/dev/null 2>&1 || true
rm -f "$pid_file"
}
stop_services() {
local service_name
local idx
for ((idx = ${#app_services[@]} - 1; idx >= 0; idx--)); do
service_name="${app_services[$idx]}"
stop_service "$service_name"
done
stop_service "$config_service"
}
status_services() {
local service_name
local pid
local log_file
printf '%-36s %-10s %s\n' "service" "pid" "log"
for service_name in "${all_services[@]}"; do
pid="$(read_service_pid "$service_name" || true)"
log_file="$(service_log_file "$service_name")"
if is_pid_running "$pid"; then
printf '%-36s %-10s %s\n' "$service_name" "$pid" "$log_file"
else
printf '%-36s %-10s %s\n' "$service_name" "-" "$log_file"
fi
done
}
tail_logs() {
local service_name="${1:-}"
local log_file
if [[ -z "$service_name" ]]; then
echo "Service name is required for logs command." >&2
exit 1
fi
log_file="$(service_log_file "$service_name")"
if [[ ! -f "$log_file" ]]; then
echo "Log file does not exist: $log_file" >&2
exit 1
fi
tail -f "$log_file"
}
start_all() {
require_command java
build_services
if [[ "${START_INFRA:-true}" == "true" ]]; then
start_infra
if [[ "${INIT_DB:-true}" == "true" ]]; then
init_databases
fi
else
echo "Skipping local infra startup. Expecting Postgres at $postgres_host:$postgres_port and Kafka at $kafka_host:$kafka_port."
fi
start_config_server
start_app_services
echo "Local jar startup requested."
status_services
}
command="${1:-start}"
case "$command" in
start)
start_all
;;
restart)
stop_services
start_all
;;
stop)
stop_services
;;
status)
status_services
;;
build)
build_services
;;
infra-up)
start_infra
if [[ "${INIT_DB:-true}" == "true" ]]; then
init_databases
fi
;;
infra-down)
require_command docker
compose stop kafka postgres
;;
logs)
tail_logs "${2:-}"
;;
help|-h|--help)
usage
;;
*)
usage >&2
exit 1
;;
esac
+266
View File
@@ -0,0 +1,266 @@
# Observatio Terrae (pcp) — карта сервисов, взаимодействие и контракты
> Базовый справочный документ для доработки системы планирования ДЗЗ.
> Составлен из исходного кода (контроллеры, Kafka listeners/publishers, HTTP-клиенты) и `config-repo`.
> Помечено явно, что **проверено по коду**, а что **требует уточнения** (`⚠`).
Стек: Kotlin + Spring Boot, Spring Cloud Config, Spring Boot Admin, Camunda 8 / Zeebe (оркестрация), Kafka (события), PostgreSQL (БД на сервис, Flyway), Helm + GitLab CI.
---
## 1. Карта сервисов — кто за что отвечает
### Доменные сервисы
| Сервис | Зона ответственности | REST base | Хранит | Kafka (in / out) | Зрелость* |
|---|---|---|---|---|---|
| **pcp-request-service** | Приём и жизненный цикл заявок на съёмку; сетка Земли (earth-grid), геопривязка ячеек, сопоставление маршрутов с заявками | `/v1/requests`, `/v1/cells`, `/v1/grid` | заявки, ячейки сетки, outbox | in: `pcp.route.georeference.v1` · out: `pcp.request.status.v1` (outbox → geoportal) | 🟢 высокая |
| **pcp-route-processing-service** | Приём «паспортов маршрутов», нормализация/геопривязка, ретрансляция дальше; собственный outbox + DLQ | `/api/route-processing` (test) | outbox маршрутов | in: `pcp.request.survey-georeference.v1` · out: `pcp.route.georeference.v1`, `pcp.route.in.v1`, DLQ | 🟢 высокая |
| **pcp-satellite-catalog-service** | Каталог КА (источник истины): спутники, группы, observation/slot-профили | `/api/satellites`, `/api/satellite-groups` | каталог КА, профили | out: `pcp.satellites` (`SatelliteDeletedEvent`) | 🟡 средняя (мало тестов) |
| **pcp-ballistics-service** | Баллистика: орбиты, ЗРВ/RVA, узлы, трассы, time-extract по TLE | `/api/satellites`, `/api/obj-view`, `/api/tle`, `/api/rva` | орбитальные данные | in: `pcp.tle` (IC/ICRV/TLE фильтры), `pcp.satellites` | 🟡 средняя |
| **tle-monitoring-service** | Подтяжка TLE (CelesTrak), мониторинг каталога | — | TLE-история | in: `pcp.satellites` · out: `pcp.tle` ⚠ | 🟡 средняя |
| **pcp-coverage-scheme-service** | Расчёт схемы покрытия (coverage scheme) | `/api/coverage-schemes` | результаты схем | in: `pcp.satellites` | 🟡 средняя |
| **slots-service** | Слоты сброса/ресурсы: расчёт, бронирование, статусы слотов | (см. контроллеры) | слоты, брони | in/out: `pcp.slots.status.v1` (`BookedSlotsStatusChangedEvent`); in: `pcp.satellites` | 🟢 высокая (лучшее покрытие тестами) |
| **pcp-complex-mission-service** | Комплексный план по тепловой карте методом слотов; снапшоты планов | `/api/satellites`, `/api/com-plan` | комплексные планы, снапшоты | in: `pcp.satellites` | 🟢 высокая |
| **pcp-mission-planing-service** | План по конкретному КА: съёмка / сброс / стирание; **хост Zeebe-воркеров** оркестрации | `/api/missions` | миссии, режимы | in: `pcp.slots.status.v1`, `pcp.route.in.v1`, `pcp.satellites` | 🔴 низкая (есть заглушки в `FlowService`) |
| **pcp-dynamic-plan-service** | Динамический/текущий план (greedy-математика, расчёт по интервалам) | `/v1/main/calcPlan` | прогоны (runs), результаты | in: `pcp.satellites` | 🟡 средняя |
| **pcp-tgu-service** | Операторская оркестрация (ТГУ/Mission Ops): окна видимости, выдача планов, решения по планам; **деплой и запуск BPMN** | `/api/plans`, `/api/platforms`, `/test/tgu` | планы КА, dedup-таблицы | in: `pcp.tgu.satellite-plan-decision.v1` · out: `pcp.tgu.visibility-windows-changed.v1` | 🟢 высокая |
| **pcp-stations-service** | Каталог наземных станций приёма | `/api/stations` | станции | — | 🟡 средняя |
| **pcp-ui-service** | BFF/агрегатор для веб-UI и 3D-глобуса (CZML/Cesium); проксирует во все сервисы | `/requests`, `/map`, `/api/*` | — (stateless) | — | 🟡 средняя |
\* Зрелость = оценка по соотношению тестов, наличию idempotency/outbox, чистоте слоёв и отсутствию заглушек (см. предыдущий разбор).
### Инфраструктурные модули
| Модуль | Роль |
|---|---|
| **spring-cloud-config-server** | Раздаёт конфигурацию из `config-repo` всем сервисам |
| **pcp-srpring-boot-admin-server** | Spring Boot Admin — мониторинг/health всех сервисов (опечатка в имени: `srpring`) |
| **pcp-tgu-ops-ui** | Отдельная SPA Mission Ops/TGU; карта возможностей и источников данных: [`PCP_TGU_OPS_UI_CAPABILITY_MAP.md`](PCP_TGU_OPS_UI_CAPABILITY_MAP.md) |
| **libs/pcp-types-lib** | Общие DTO и конверт `KafkaMessage<T>`, перечисление `PcpKafkaEvent` |
| **libs/ballistics-lib** | Общие баллистические расчёты |
---
## 2. Диаграммы взаимодействия
### 2.1. Синхронные зависимости (REST) — кто кого вызывает
Рёбра восстановлены из ключей `*-service*` base-url в `config-repo` и имён HTTP-клиентов в коде.
```mermaid
flowchart TD
UI[pcp-ui-service<br/>BFF / агрегатор]
REQ[pcp-request-service<br/>заявки + earth-grid]
CAT[pcp-satellite-catalog-service<br/>каталог КА]
BAL[pcp-ballistics-service]
CM[pcp-complex-mission-service]
MP[pcp-mission-planing-service]
SL[slots-service]
CS[pcp-coverage-scheme-service]
DP[pcp-dynamic-plan-service]
TGU[pcp-tgu-service]
ST[pcp-stations-service]
TLE[tle-monitoring-service]
UI --> REQ & CM & DP & TGU & CAT & BAL & ST & SL & MP & CS
CM --> BAL & REQ & CAT & ST & SL & CS
MP --> BAL & CM & CAT & ST
SL --> REQ & CAT
DP --> REQ & BAL & CAT
CS --> BAL & CM & CAT
TLE --> CM & CAT
TGU --> BAL
TLE -.внешний.-> CTRAK[(CelesTrak)]
TGU -.внешний.-> ORD[(ordinis.nstart.cloud<br/>классификатор платформ + external points)]
```
Наблюдения по графу:
- `pcp-satellite-catalog-service` и `pcp-ballistics-service` — самые востребованные (почти все зовут их). Кандидаты на повышенные требования к SLA/надёжности.
- `pcp-ui-service` — точка агрегации (BFF), завязан на всё.
- ⚠ Есть взаимный вызов `complex-mission ⇄ coverage-scheme` (CM зовёт CS на расчёт, CS в конфиге знает про CM) — проверить, нет ли циклической зависимости в рантайме.
### 2.2. Событийный слой (Kafka) — потоки данных
```mermaid
flowchart LR
EXT[Внешний источник / UI ⚠] -->|pcp.request.survey-georeference.v1| RP[route-processing]
RP -->|pcp.route.georeference.v1| REQ[request-service]
RP -->|pcp.route.in.v1| MP[mission-planing]
REQ -->|pcp.request.status.v1 · COMPLETED/EXPIRED| GEO[geoportal · внешний]
CAT[satellite-catalog] -->|pcp.satellites / SatelliteDeletedEvent| BAL[ballistics]
CAT --> TLEM[tle-monitoring]
CAT --> SL[slots]
CAT --> MP
CAT --> CM[complex-mission]
CAT --> DP[dynamic-plan]
TLEM -->|pcp.tle| BAL
SL -->|pcp.slots.status.v1 / BookedSlotsStatusChanged| MP
SL -->|pcp.slots.status.v1| SL
MissionOps[Mission Ops / КА ⚠] -->|pcp.tgu.satellite-plan-decision.v1| TGU[tgu-service]
TGU -->|pcp.tgu.visibility-windows-changed.v1| TGU
```
### 2.3. Сквозной сценарий планирования (sequence)
Упрощённый happy-path от заявки до выдачи задания на КА.
```mermaid
sequenceDiagram
participant U as UI
participant RQ as request-service
participant RP as route-processing
participant CM as complex-mission
participant SL as slots-service
participant TGU as tgu-service (Camunda)
participant MP as mission-planing (Zeebe workers)
U->>RQ: POST заявка на съёмку
RP-->>RQ: pcp.route.georeference.v1 (геопривязанные маршруты)
RQ->>RQ: сопоставление маршрут↔заявка, RequestCompleted (outbox)
U->>CM: POST /api/com-plan/process (комплексный план)
CM->>SL: расчёт/бронирование слотов
CM->>CM: снапшот комплексного плана
U->>TGU: POST /test/tgu/plans/{id}/issue
TGU->>TGU: start BPMN createSatelliteMission
Note over TGU,MP: Zeebe job dispatch по jobType
TGU-->>MP: calculateSatelliteSurveyMissions
MP->>MP: план съёмки
TGU-->>MP: calculateSatelliteDropMissions
MP->>MP: план сброса
TGU-->>MP: sendSatellitePlan
MP-->>TGU: Message_SatellitePlanAccepted ⚠ (сейчас имитация)
MissionOps-->>TGU: pcp.tgu.satellite-plan-decision.v1 (ACCEPTED/REJECTED)
TGU->>TGU: updateAcceptedPlanSlots BPMN → updateSlotsStatus
```
---
## 3. Контракты между сервисами
### 3.1. Оркестрация Camunda 8 / Zeebe
**Деплой BPMN:** `pcp-tgu-service` через `@Deployment` при старте грузит:
`classpath:BPMN/createSatelliteMission.bpmn`, `classpath:BPMN/updateAcceptedPlanSlots.bpmn`.
**Zeebe job workers** (реализованы в `pcp-mission-planing-service/FlowService`):
| jobType | Назначение | Вход (process variables) | Выход | Примечание |
|---|---|---|---|---|
| `calculateSatelliteSurveyMissions` | План съёмки | `satellitePlanId`, `spacecraftId`, `planStartTime`, `planEndTime` | `modes: Int` | `autoComplete=false` |
| `calculateSatelliteDropMissions` | План сброса | `satellitePlanId` | `modes: Int` | — |
| `sendSatellitePlan` | Отправка плана на КА | `satellitePlanId` | — | ⚠ сейчас `Thread.sleep` + имитация подтверждения |
| `updateSlotsStatus` | Подтверждение, обновление слотов | `satellitePlanId` | `slots: List` | используется в `updateAcceptedPlanSlots` |
**Camunda message:** `Message_SatellitePlanAccepted`, `correlationKey = satellitePlanId`.
> ⚠ `schemes/pcp_v1.bpmn` — большой концептуальный процесс (шлюзы, параллельные ветки, подпроцессы), **не задеплоен** и без jobType. Реально работает только минимальный `createSatelliteMission.bpmn` (start → 3 service task → end, без обработки ошибок).
### 3.2. Kafka-топики (реестр)
Конверт по умолчанию — `KafkaMessage<T>` из `pcp-types-lib`: поля `type`, `data`, `traceId`, `correlationId`, `id`, `source`, `time`. Тип события — enum `PcpKafkaEvent`.
| Топик | Producer | Consumer(s) | Тип события / payload | Ключ |
|---|---|---|---|---|
| `pcp.satellites` | satellite-catalog | ballistics, tle-monitoring, slots, mission-planing, complex-mission, dynamic-plan | `SatelliteDeletedEvent` (`satelliteId`, `noradId`) | `satelliteId` |
| `pcp.tle` | tle-monitoring / catalog ⚠ | ballistics (фильтры IC/ICRV/TLE) | TLE / `ICPlacedEvent` / `ICRVPlacedEvent` / `ICUpdatedEvent` | ⚠ |
| `pcp.request.survey-georeference.v1` | внешний / UI ⚠ | route-processing | RoutePassport (сырой) | ⚠ |
| `pcp.request.survey-georeference-dlq.v1` | route-processing | (DLQ) | — | — |
| `pcp.route.georeference.v1` | route-processing | request-service | `RoutePassportDto` (геопривязанный) | ⚠ |
| `pcp.route.in.v1` | route-processing | mission-planing (`ModeStatusChangedEvent`) | processed route / mode-status | ⚠ |
| `pcp.request.in.v1` | request-service / UI ⚠ | ⚠ требует уточнения | `RequestPlacedEvent` | ⚠ |
| `pcp.request.status.v1` | request-service (outbox) | **geoportal** (внешний) | `REQUEST_TERMINAL_STATUS` (COMPLETED/EXPIRED) | `aggregateId` (UUID заявки) |
| `pcp.slots.status.v1` | slots-service | mission-planing, slots-service (self) | `BookedSlotsStatusChangedEvent` | ⚠ |
| `pcp.tgu.satellite-plan-decision.v1` | Mission Ops / КА ⚠ | tgu-service | `PlanDecisionMessage` (ACCEPTED/REJECTED, `attemptId`) | ⚠ |
| `pcp.tgu.visibility-windows-changed.v1` | tgu-service | tgu-service | `VisibilityWindowsChangedMessage` | ⚠ |
> ⚠ Реализация консьюмеров неоднородна: одни читают типизированный `KafkaMessage<T>`, другие парсят сырое дерево и ждут поле `data`; используются обе версии Jackson (2 и 3). Это первый кандидат на унификацию контракта.
### 3.3. REST-эндпоинты (контракты API по сервисам)
**request-service** (`/v1`)
```
GET /v1/requests/{id}
GET /v1/requests/{requestId}/with-cells
DELETE /v1/requests/{id}
GET /v1/cells/with-requests
GET /v1/grid/settings PUT /v1/grid/settings POST /v1/grid/rebuild
```
**satellite-catalog-service** (`/api/satellites`, `/api/satellite-groups`)
```
GET/PUT/DELETE /api/satellites/{satellite_id}
GET /api/satellites/by-norad/{norad_id}
GET/POST/PUT/DELETE /api/satellites/{satellite_id}/observation-profile
GET/POST/PUT/DELETE /api/satellites/{satellite_id}/slot-profile
POST /api/satellites/batch
GET/PUT/DELETE /api/satellite-groups/{group_id}
```
**ballistics-service** (`/api`)
```
POST /api/satellites/{satellite_id}/extract-time POST /api/satellites/{satellite_id}/clear
GET /api/satellites/{norad_id}/orbit | /asc-node | /flight-line | /rva
GET /api/satellites/orbit/availability[/{id}]
POST /api/obj-view/mpl-square | /mpl-point GET /api/obj-view/cell-coverage/{number}
POST /api/tle/parse | /api/tle/rva
```
**complex-mission-service** (`/api`)
```
POST /api/com-plan/process | /clear GET/DELETE /api/com-plan/runs/{id}
GET /api/com-plan/runs | /runs/{id}/modes | /cell-with-mars/{cell_number}
GET /api/satellites/{satellite_id}[/mission|/modes|/snapshots|/mission/statistics|/mission-for-paint]
GET /api/satellites/snapshots[/{snapshot_id}[/modes]] | /modes POST /api/satellites/prepare
```
**mission-planing-service** (`/api/missions`)
```
GET /api/missions/{mission_id}[/modes] GET /api/missions/modes/satellite
POST /api/missions/{mission_id}/drops/calculate POST /api/missions/{mission_id}/surveys/calculate
POST /api/missions/{mission_id}/confirm PUT /api/missions/survey-modes/{mode_id}/status
PUT/DELETE /api/missions/{mission_id}
```
**slots-service / coverage-scheme / dynamic-plan / stations / tgu**
```
coverage-scheme : POST /api/coverage-schemes/calculate
dynamic-plan : POST /v1/main/calcPlan ; GET /v1/main/calcPlan/runs|/{runId}[/result|/routes|/intervals]
stations : GET /api/stations[/{station_id}] ; POST /api/stations ; DELETE /api/stations/{station_id}
tgu : GET /api/plans/{spacecraftId} ; /api/platforms ;
POST /test/tgu/plans/{planId}/issue|/decision ; /spacecraft/{id}/refresh-visibility|/rebuild-plans
```
**ui-service** — BFF, агрегирует вызовы: `/requests/*`, `/map`, `/api/requests`, `/api/slots/booking/request`,
`/api/com-plan/*`, `/api/tgu-planning/*`, `/api/current-plans/*`, `/api/dynamic-plan/*`, `/api/stations/*`.
### 3.4. Внешние интеграции
| Откуда | Куда | Назначение |
|---|---|---|
| tle-monitoring-service | CelesTrak | загрузка TLE |
| tgu-service | `https://ordinis.nstart.cloud/api/v1/spacecraft/records` | классификатор платформ (фильтр `OPERATIONAL`), кэш 15m |
| tgu-service | external points API (`pcp.services.ballistics`) | внешние точки (lookahead 3 дня) |
---
## 4. Открытые вопросы для уточнения перед доработкой
Эти пункты помечены `⚠` выше — их стоит зафиксировать до изменений:
1. Producer топиков `pcp.request.survey-georeference.v1` и `pcp.request.in.v1` (внешняя система или UI?).
2. ~~Consumer `pcp.request.completed.v1`~~ — уточнено: потребитель терминальных статусов заявки = внешний **geoportal**, топик переименован в `pcp.request.status.v1` (COMPLETED/EXPIRED, #35).
3. Producer `pcp.tgu.satellite-plan-decision.v1` — реальный Mission Ops/КА vs текущая имитация в `FlowService`.
4. Точные DTO payload для `pcp.tle`, `pcp.route.in.v1`, `pcp.slots.status.v1` — зафиксировать схемы.
5. Возможная циклическая зависимость `complex-mission ⇄ coverage-scheme` в рантайме.
6. Расхождение задеплоенного `createSatelliteMission.bpmn` и концептуального `schemes/pcp_v1.bpmn` — какой считать целевым.
---
*Документ описывает текущее состояние «как есть». При доработке обновлять разделы 2–3 синхронно с изменениями контроллеров, listener'ов и `config-repo`.*
+224
View File
@@ -0,0 +1,224 @@
# Карта взаимодействий сервисов PCP
> Составлено по исходникам (`services/*/src/main`) и конфигам (`config-repo/`).
> Три канала интеграции: **Kafka** (события), **HTTP/REST** (синхронные запросы), **Camunda 8 / Zeebe** (оркестрация процессов).
> Имена топиков и адреса сервисов берутся из Spring Cloud Config (`config-repo/*.yaml`,
> общая карта адресов — `application-dev.yaml` / `application-local.yaml`, ключи `pcp.services.*`).
## Содержание
1. [Перечень сервисов и порты](#1-перечень-сервисов-и-порты)
2. [Kafka — карта топиков и событий](#2-kafka--карта-топиков-и-событий)
3. [HTTP/REST — карта вызовов между сервисами](#3-httprest--карта-вызовов-между-сервисами)
4. [Camunda — процессы и воркеры](#4-camunda--процессы-и-воркеры)
---
## 1. Перечень сервисов и порты
Локальные порты — из `config-repo/application-local.yaml` (`pcp.ports.*`). В dev/docker все слушают `8080`.
| Сервис | Локальный порт | Роль |
|---|---|---|
| `spring-cloud-config-server` | 8888 | раздача конфигов |
| `pcp-srpring-boot-admin-server` | (admin) | мониторинг Spring Boot Admin |
| `tle-monitoring-service` | 7001 | загрузка/раздача TLE (CelesTrak) |
| `pcp-complex-mission-service` | 7002 | комплексные планы/миссии, режимы съёмки КА |
| `pcp-ballistics-service` | 7003 | баллистика, расчёт НУ/витков/трасс (он же `ic-service`) |
| `pcp-request-service` | 7005 | заявки, маршруты-привязки, outbox |
| `slots-service` | 7006 | слоты, бронирования, расчёт покрытия |
| `pcp-ui-service` | 7008 | BFF/агрегатор для старого UI |
| `pcp-stations-service` | 7009 | каталог наземных станций |
| `pcp-mission-planing-service` | 7010 | планирование миссий + **Camunda job-воркеры** |
| `pcp-tgu-service` | 7011 | ТГУ: выдача планов, **запуск Camunda-процессов** |
| `pcp-coverage-scheme-service` | 7012 | расчёт схем покрытия |
| `pcp-satellite-catalog-service` | 7013 | каталог КА |
| `pcp-dynamic-plan-service` | 7014 | динамические/комплексные планы (calcPlan) |
| `pcp-route-processing-service` | — | обработка маршрутных паспортов |
`pcp-tgu-ops-ui` — фронтенд (Vite), ходит в микросервисы напрямую через прокси, в этой карте не участвует как backend.
---
## 2. Kafka — карта топиков и событий
Событие — JSON-конверт `KafkaMessage` с заголовком `type` (значения из `PcpKafkaEvent`).
Консьюмеры фильтруют по этому заголовку.
### Топики
| Топик | Продюсер(ы) | Консьюмер(ы) | Событие / назначение |
|---|---|---|---|
| `pcp.tle` *(default-topic)* | `tle-monitoring-service` (TLE); `slots-service` (`ICRVPlacedEvent`) | `pcp-ballistics-service` (фильтры `placedIcFilter`, `updatedIcFilter`, `placedIcRvFilter`) | общий «шина TLE / НУ-РВ»: новые TLE и заложенные начальные условия |
| `pcp.satellites` | `pcp-satellite-catalog-service` (`SatelliteDeletedEvent`) | `tle-monitoring`, `slots-service`, `pcp-complex-mission-service`, `pcp-dynamic-plan-service`, `pcp-ballistics-service`, `pcp-mission-planing-service` | удаление КА (fan-out для очистки производных данных) |
| `pcp.slots.status.v1` | `pcp-mission-planing-service` (`BookedSlotsStatusChangedEvent`) | `slots-service` (`BookedSlotsKafkaListener`) | смена статуса забронированных слотов (lifecycle Kafka-first) |
| `pcp.route.in.v1` | `pcp-route-processing-service` (`processed-route`) | `pcp-mission-planing-service` (`ModeStatusChangedKafkaListener`, `ModeStatusChangedEvent`) | обработанный маршрут → смена статуса режима съёмки |
| `pcp.route.georeference.v1` | `pcp-route-processing-service` (`route-georeference`) | `pcp-request-service` (`RequestKafkaListener.consumeRoute`) | геопривязка маршрута к заявке |
| `pcp.request.survey-georeference.v1` | (инжект `RouteProcessingTestController` / внешний источник) | `pcp-route-processing-service` (`RoutePassportMessageListener`, вход) | входной маршрутный паспорт на обработку |
| `pcp.request.survey-georeference-dlq.v1` | `pcp-route-processing-service` (DLQ) | — | dead-letter для входных паспортов |
| `pcp.request.status.v1` | `pcp-request-service` (`OutboxPublisherService`, outbox) | **geoportal** (внешний, в репо нет) | терминальный статус заявки `COMPLETED`/`EXPIRED` (`REQUEST_TERMINAL_STATUS`) |
| `pcp.request.in.v1` | — | *(объявлен в конфиге `pcp-request-service`, активного слушателя нет)* | входящие заявки (legacy/зарезервировано) |
### Поток данных (упрощённо)
```
CelesTrak ──HTTP──> tle-monitoring ──pcp.tle(TLE)──────────────> ballistics
slots-service ─────pcp.tle(ICRVPlacedEvent)───────────────────> ballistics
satellite-catalog ──pcp.satellites(SatelliteDeletedEvent)──┬──> tle-monitoring
├──> slots-service
├──> complex-mission
├──> dynamic-plan
├──> ballistics
└──> mission-planing
route-processing ──pcp.route.in.v1──────> mission-planing (ModeStatusChanged)
route-processing ──pcp.route.georeference.v1──> request-service
(вход) pcp.request.survey-georeference.v1 ──> route-processing
mission-planing ──pcp.slots.status.v1(BookedSlots)──────> slots-service
request-service ──pcp.request.status.v1 (outbox)────────> geoportal (внешний)
```
### ТГУ-топики (`pcp.tgu.*`)
`pcp-tgu-service` объявляет консьюмеры на свои топики (`config-repo/pcp-tgu-service.yaml`, блок `topics:`).
Продюсеры этих событий — внешняя система СПО УОГ / тестовые эндпоинты; внутри репозитория стабильных продюсеров нет.
| Топик | Консьюмер в tgu | Событие |
|---|---|---|
| `pcp.tgu.visibility-windows-changed.v1` | `VisibilityWindowsChangedConsumer` | изменение окон видимости |
| `pcp.tgu.satellite-plan-decision.v1` | `PlanDecisionConsumer` | решение по плану (принят/отклонён) |
| `pcp.tgu.layin-confirmed.v1` | `LayinConfirmedConsumer` | подтверждение закладки |
| `pcp.tgu.layin-failed.v1` | `LayinFailedConsumer` | ошибка закладки |
| `pcp.tgu.plan-not-accepted.v1` | `PlanNotAcceptedConsumer` | план не принят |
| `pcp.tgu.station-params-changed.v1` | `StationParamsChangedConsumer` | смена параметров станции |
---
## 3. HTTP/REST — карта вызовов между сервисами
Адреса берутся из `settings.*``pcp.services.*` (Spring Cloud Config). Клиенты — `WebClient`.
| Вызывающий сервис | Целевой сервис | Эндпоинты (примеры) | Назначение |
|---|---|---|---|
| `pcp-ui-service` (BFF) | `tle-monitoring` | `/v1/api/tle`, `/v1/api/tle/satellite/{id}` | TLE |
| | `pcp-satellite-catalog` | `/api/satellites`, `/api/satellites/{id}/observation-profile`, `/slot-profile`, `/api/satellite-groups` | каталог КА |
| | `pcp-complex-mission` | `/api/missions`, `/api/missions/{id}/drops/calculate`, `/api/com-plan/runs`, `/api/com-plan/process` | миссии и комплексные планы |
| | `pcp-dynamic-plan` | `/v1/main/calcPlan`, `/{runId}/result`, `/routes`, `/intervals` | динамические планы |
| | `pcp-request` (request / earth-grid) | `/v1/requests`, `/v1/requests/{id}/cells` | заявки и ячейки |
| | `pcp-stations` | `/api/stations`, `/api/stations/{id}` | станции |
| | `slots-service` | `/api/slots/...`, `/api/slots/booking[...]` | слоты и бронирования |
| | `pcp-ballistics` | `/api/satellites/{id}/asc-node`, `/flight-line`, `/orbit`, `/extract-time`, `/initial-conditions/current`, `/receive-rv`, `/api/tle/parse`, `/api/tle/point` | баллистика |
| | `pcp-coverage-scheme` | `/api/coverage-schemes/calculate` | схемы покрытия |
| | `pcp-tgu` | `/api/platforms`, `/api/plans[...]`, `/api/plans/{id}/dismiss`, `/restore`, `/test/tgu/plans/{id}/decision`, `/confirm-layin` | планы ТГУ |
| `pcp-tgu-service` | `pcp-mission-planing` (`mission-planing.base-url`) | `/api/missions/{id}/clone`, `/api/missions/blank`, `/api/missions/{id}` | управление миссиями плана |
| | `pcp-ballistics` (`ballistics.base-url`) | расчёт точек/баллистики (`ExternalPointsClient`) | внешние точки |
| | `ordinis.nstart.cloud` (внешний) | `/api/v1/spacecraft/records` (`platforms-url`) | классификатор платформ КА |
| `pcp-mission-planing` | `pcp-ballistics` | `/api/obj-view/mpl-square`, `/mpl-point`, `/cell-coverage/{cell}`, `/api/satellites/{id}/rva`, `/extract-time` | баллистика для планирования |
| | `pcp-complex-mission` | `/api/satellites/snapshots/{id}`, режимы КА | данные комплексной миссии |
| | `pcp-satellite-catalog` | `/api/satellites/{id}` | каталог КА |
| `pcp-complex-mission` | `pcp-ballistics` (`ic-service`) | расчёты НУ/баллистики | баллистика |
| | `pcp-satellite-catalog` | `/api/satellites`, `/api/satellites/batch` | каталог КА |
| | `slots-service` | покрытие/слоты (`CoverageBatchClient`, `SlotService`) | слоты |
| | `pcp-request` (`earth-grid`) | сетка Земли/ячейки (`EarthGridService`) | заявки/ячейки |
| | `pcp-coverage-scheme` | расчёт схемы покрытия | покрытие |
| `pcp-dynamic-plan` | `pcp-request` | `/v1/requests` (выборка заявок) | заявки |
| | `pcp-ballistics` | `/api/satellites/orbit/availability/{norad}`, `/extract-time`, `/api/obj-view/mpl-square` | баллистика |
| | `pcp-satellite-catalog` | `/api/satellites` | каталог КА |
| `pcp-coverage-scheme` | `pcp-ballistics` | `/api/obj-view/mpl-square`, `/mpl-point`, `/extract-time` | баллистика |
| | `pcp-complex-mission` | `/api/satellites/{id}` | данные КА/миссии |
| `pcp-ballistics` | `pcp-stations` | `/api/stations` | станции (для расчётов видимости) |
| `slots-service` | `pcp-satellite-catalog` | `/api/satellites`, `/batch`, `/slot-profile` | каталог КА |
| | `pcp-request` (`earth-grid`) | `/v1/requests`, `/cells` | заявки/ячейки |
| `tle-monitoring` | `pcp-satellite-catalog` | `/api/satellites` | каталог КА |
Все сервисы дополнительно ходят в `spring-cloud-config-server` (8888) за конфигом и регистрируются в `spring-boot-admin-server`.
### Граф зависимостей (кто кого зовёт по HTTP)
```
ui-service ──> почти все (BFF-агрегатор)
tgu ─────────> mission-planing, ballistics, (ordinis ext)
mission-planing ─> ballistics, complex-mission, satellite-catalog
complex-mission ─> ballistics, satellite-catalog, slots, request, coverage-scheme
dynamic-plan ───> request, ballistics, satellite-catalog
coverage-scheme ─> ballistics, complex-mission
ballistics ─────> stations
slots ──────────> satellite-catalog, request
tle-monitoring ─> satellite-catalog (+ CelesTrak внешний)
```
`pcp-ballistics` и `pcp-satellite-catalog` — наиболее востребованные «нижние» сервисы.
---
## 4. Camunda — процессы и воркеры
Используется **Camunda 8 (Zeebe)**, `io.camunda:camunda-spring-boot-4-starter:8.8.10`.
Брокер внешний: gRPC `camunda.k8s.265.nstart.cloud:30901` (dev) / `localhost:26500` (локально),
ключ `camunda.client.grpc-address`.
- **`pcp-tgu-service`** — *оркестратор*: содержит BPMN-определения, разворачивает их и запускает экземпляры процессов (`CamundaClient.newCreateInstanceCommand()`).
- **`pcp-mission-planing-service`** — *исполнитель*: реализует все job-воркеры (`@JobWorker` в `FlowService`).
### Процесс `createSatelliteMission`
BPMN: [createSatelliteMission.bpmn](../../services/pcp-tgu-service/src/main/resources/BPMN/createSatelliteMission.bpmn).
Запуск: `BpmnPlanIssueProcessStarter``IssueDuePlanWorker.issuePlanNow()``PlanController` `POST /api/plans/{planId}/issue`.
```
startSatelliteMission
→ [calculateSatelliteSurveyMissions] «Сформировать план съёмки»
→ [calculateSatelliteDropMissions] «Сформировать план сбросов»
→ [sendSatellitePlan] «Отправка плана в СПО УОГ»
→ endSatelliteMission
```
### Процесс `updateAcceptedPlanSlots`
BPMN: [updateAcceptedPlanSlots.bpmn](../../services/pcp-tgu-service/src/main/resources/BPMN/updateAcceptedPlanSlots.bpmn).
Запуск: `AcceptedPlanSlotsUpdateProcessStarter``AcceptedPlanSlotsUpdateWorker`.
```
start → [updateSlotsStatus] «Обновление статуса слотов» → end
```
### Соответствие job-type → воркер
Все воркеры — в `pcp-mission-planing-service`, [FlowService.kt](../../services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/FlowService.kt), `autoComplete = false` (ручной `newCompleteCommand`).
| Job type (BPMN) | Метод воркера | Процесс |
|---|---|---|
| `calculateSatelliteSurveyMissions` | `FlowService.surveyPlan` | createSatelliteMission |
| `calculateSatelliteDropMissions` | `FlowService.surveyDrop` | createSatelliteMission |
| `sendSatellitePlan` | `FlowService.sendPlan` | createSatelliteMission |
| `updateSlotsStatus` | `FlowService.confirmPlan` | updateAcceptedPlanSlots |
### Схема
```
pcp-tgu-service pcp-mission-planing-service
(оркестратор) (job-воркеры, FlowService)
PlanController POST /api/plans/{id}/issue
BpmnPlanIssueProcessStarter
│ newCreateInstanceCommand("createSatelliteMission")
┌─────────────── Zeebe / Camunda 8 broker (gRPC) ───────────────┐
│ calculateSatelliteSurveyMissions ─────────────────────────► surveyPlan
│ calculateSatelliteDropMissions ─────────────────────────► surveyDrop
│ sendSatellitePlan ─────────────────────────► sendPlan
└───────────────────────────────────────────────────────────────┘
AcceptedPlanSlotsUpdateProcessStarter
│ newCreateInstanceCommand("updateAcceptedPlanSlots")
┌─────────────── Zeebe broker ───────────────┐
│ updateSlotsStatus ───────────────────────► confirmPlan
└─────────────────────────────────────────────┘
```
> Связь между HTTP и Camunda: воркеры `pcp-mission-planing-service` в ходе выполнения задач
> сами ходят по HTTP в `pcp-ballistics`, `pcp-complex-mission`, `pcp-satellite-catalog`
> и публикуют Kafka-событие `pcp.slots.status.v1` (см. разделы 23).
@@ -0,0 +1,493 @@
# pcp-tgu-ops-ui — карта возможностей и источников данных
Дата анализа: 2026-05-31.
Документ фиксирует, что сейчас умеет `pcp-tgu-ops-ui`: что отображает, чем управляет, какие backend endpoints вызывает и откуда реально приходят данные. Анализ выполнен по исходному коду frontend и по execution path backend endpoints до service/repository/external source.
## Кратко
`pcp-tgu-ops-ui` — отдельная Vite/React SPA для Mission Ops/TGU. Приложение рендерит `TguPlanningPage` и имеет три вкладки:
- `Планы` — timeline persisted TGU-планов по КА.
- `Карта 2D` — карта с полигонами заявок на съемку.
- `Редактор` — локальный редактор включений КА с черновиком, проверками и заглушкой сохранения.
Реально изменять состояние backend UI умеет только через отправку решения по плану: `ACCEPTED` или `REJECTED`. Редакторские правки пока не сохраняются во внешний сервис.
## Общая схема
```text
Browser
|
v
pcp-tgu-ops-ui SPA
|
|-- /api/tgu-planning/*
| |
| v
| pcp-ui-service :7008
| |
| v
| pcp-tgu-service :7011
| |
| |-- pcp_tgu DB
| |
| `-- Ordinis NSI spacecraft classifier
|
|-- /api/current-plans/*
| |
| v
| pcp-ui-service :7008
| |
| v
| pcp-mission-planing-service :7010
| |
| `-- mission planning DB
|
|-- /api/pcp-request/v1/*
| |
| v
| pcp-request-service :7005
| |
| `-- request DB
|
`-- https://*.basemaps.cartocdn.com
|
`-- CARTO basemap tiles
```
Dev proxy:
- `/api/pcp-request` -> `http://localhost:7005`, prefix `/api/pcp-request` stripped.
- `/api` -> `http://localhost:7008`.
Production nginx:
- `/api/pcp-request/` -> `http://host.docker.internal:7005/`.
- `/api/` -> `http://host.docker.internal:7008/api/`.
## Endpoint inventory
| UI endpoint | Метод | Использование в UI | Backend execution path | Источник |
|---|---:|---|---|---|
| `/api/tgu-planning/platforms` | GET | список КА, подписи, поиск, редактор | `pcp-ui-service` `TguPlanningRestController.platforms()` -> `TguPlanningService.platforms()` -> `pcp-tgu-service /api/platforms` -> `PlatformService.loadPlatforms()` | in-memory cache, Ordinis NSI |
| `/api/tgu-planning/plans?historyDays=N` | GET | загрузка всех TGU-планов для timeline/map/editor context | `pcp-ui-service` -> `pcp-tgu-service /api/plans` -> `PlanQueryService.getAllPlans()` -> `PlannedPlanRepository.findAllByOrderByStartTime()` | `planned_plan` |
| `/api/tgu-planning/spacecraft/{spacecraftId}/plans?historyDays=N` | GET | есть в API-клиенте, текущая страница в основном грузит все планы | `pcp-ui-service` -> `pcp-tgu-service /api/plans/{spacecraftId}` -> `PlanQueryService.getPlansBySpacecraftId()` | `planned_plan` |
| `/api/tgu-planning/plans/{planId}/decision?decision=...&reason=...` | POST | кнопки принять/отклонить для `WAITING_DECISION` | `pcp-ui-service` -> `pcp-tgu-service /test/tgu/plans/{planId}/decision` -> `TestPlanDecisionService.generateDecision()` -> `HandlePlanDecisionUseCase.handle()` | `planned_plan`, `plan_issue_attempt`, `spacecraft_planning_state`, `processed_plan_decision_event` |
| `/api/pcp-request/v1/requests/map?...` | GET | полигоны заявок на карте 2D и в редакторе | напрямую в `pcp-request-service /v1/requests/map` -> `RequestController.listRequestsForMap()` -> `RequestService.listRequestsForMap()` -> `RequestRepository.findRequests()` | `requests` |
| `/api/current-plans/missions/{missionId}/modes` | GET | контуры существующих включений в редакторе | `pcp-ui-service` -> `pcp-mission-planing-service /api/missions/{missionId}/modes` -> `MissionPlaningService.modesByMission()` | `missions`, `plans`, `modes`, `survey_modes`, `drop_modes`, `drop_mode_survey`, `mode_booking` |
## Вкладка "Планы"
### Что отображает
- Список КА: `name`, `NORAD`, `mission`, `status`.
- Количество планов по КА в выбранном интервале.
- Индикатор проблемных статусов: `REJECTED`, `EXPIRED`, `START_AMBIGUOUS`.
- Timeline persisted TGU-планов по КА.
- Стрелки между последовательными планами одного КА.
- Линию текущего времени, если `now` попадает в выбранный диапазон.
- Детали выбранного плана:
- `planId`;
- `spacecraftId`;
- КА;
- `startTime`;
- `endTime`;
- duration;
- `kppId`;
- `status`.
### Чем управляет
- Фильтр диапазона дат.
- Быстрые диапазоны: 24 часа, 3 суток, 7 суток.
- Refresh данных.
- Поиск по КА.
- Выбор конкретного КА.
- Выбор плана.
- Отправка решения `ACCEPTED`/`REJECTED`, но только для плана в статусе `WAITING_DECISION`.
### Execution path решения по плану
```text
User
|
v
TguPlanDetails
|
|-- Принять -> decision=ACCEPTED, reason=UI_TEST_ACCEPTED
|
`-- Отклонить -> decision=REJECTED, reason from prompt
|
v
sendPlanDecision()
|
v
POST /api/tgu-planning/plans/{planId}/decision
|
v
pcp-ui-service
|
v
TguPlanningService.sendDecision()
|
v
POST pcp-tgu-service /test/tgu/plans/{planId}/decision
|
v
TestPlanDecisionService.generateDecision()
|
|-- checks plan exists
|-- checks spacecraft_planning_state exists
|-- checks activePlanId == planId
|-- checks activeAttemptId exists and belongs to plan
|-- checks attempt status STARTING/STARTED
|
v
HandlePlanDecisionUseCase.handle()
|
|-- insert processed_plan_decision_event, dedup by eventId
|-- lock plan_issue_attempt by attemptId
|-- lock planned_plan by planId
|-- lock/create spacecraft_planning_state
|
|-- ACCEPTED:
| |-- attempt=ACCEPTED
| |-- plan=ACCEPTED
| |-- currentBasePlanId=planId
| |-- activePlanId/activeAttemptId cleared
| |-- enqueue plan_recalculation_job
| `-- enqueue accepted_plan_slots_update_job
|
`-- REJECTED:
|-- attempt=REJECTED
|-- activePlanId/activeAttemptId cleared
|-- if enough lead time: plan=PLANNED, issueNotBefore=now
`-- else: plan=EXPIRED, promote next plan or enqueue rebuild
```
Важный caveat: UI отправляет решение через test-only controller `pcp-tgu-service /test/tgu/...`. Если `tgu.test-controller.enabled=false`, эта команда перестанет работать.
## Вкладка "Карта 2D"
### Что отображает
- CARTO dark basemap tiles.
- Lat/lon grid поверх тайлов.
- Полигоны заявок на съемку из `pcp-request-service`.
- Цвета заявок по `surveyType`:
- `OPTICS`;
- `RSA`;
- `COMBINED`.
- Hover tooltip по полигону.
- Выбор полигона заявки на карте.
- Детальную панель с выбранным планом и выбранным объектом карты.
### Чем управляет
- Панорамирование карты drag-ом.
- Zoom колесом и кнопками `+`/`-`.
- Reset view.
- Включение/выключение слоев.
- Загрузка заявок за отдельный интервал.
- Быстрые интервалы заявок: сегодня, 7 дней, 30 дней.
- Скрытие/показ КА в sidebar.
### Что сейчас заявлено в UI, но не имеет данных
В главной карте 2D есть переключатели слоев:
- трасса КА;
- полоса обзора;
- работы плана;
- станции;
- маркеры КА;
- заявки на съемку.
Но текущий `buildTgu2DMapScene()` наполняет только `request` polygons. Для остальных слоев рисовалки существуют, но данные в scene не передаются. UI показывает предупреждение: `Данные слоя недоступны в текущем API.`
### Execution path карты 2D
```text
User
|
v
Tgu2DMapTab
|
|-- selectedPlan / range / layers / hiddenSpacecraftIds
|
|-- load requests
| |
| v
| fetchRequestsForMap()
| |
| |-- page size = 500
| |-- max pages = 10
| |-- max loaded requests = 5000
| |-- sort = beginDateTime,asc
| |
| v
| GET /api/pcp-request/v1/requests/map
| |
| v
| pcp-request-service
| |
| v
| RequestController.listRequestsForMap()
| |
| v
| RequestService.listRequestsForMap()
| |
| v
| RequestRepository.findRequests()
| |
| |-- status filter, optional
| |-- surveyType filter, optional
| |-- kpp filter, supported by backend but not passed by current UI
| |-- highPriorityTransmit filter, supported by backend but not passed by current UI
| |-- beginFrom/beginTo/endFrom/endTo
| |-- exclude DELETED by default
| |
| v
| requests table
|
v
buildTgu2DMapScene()
|
|-- parse WKT from request.geometry
|-- supports POLYGON and MULTIPOLYGON
|-- build MapPolygon objects with layer=requests
|
v
Tgu2DMapView
|
|-- drawTiles()
| |
| `-- https://{a|b|c|d}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png
|
|-- drawBaseMap()
| |
| `-- 30 degree lat/lon grid
|
|-- drawRequests()
| |
| `-- request polygons on canvas
|
|-- createPolygonSpatialIndex()
|-- hitTestPolygons()
|
v
Canvas + hover tooltip + selected object details
```
### Фильтр заявок на карте
Для выбранного интервала `[from, to]` UI передает:
```text
endFrom = from
beginTo = to
```
То есть выбираются заявки, которые пересекают интервал карты.
## Вкладка "Редактор"
### Что отображает
- Выбранный КА и выбранный TGU-план.
- Черновик включений:
- `shoot` — съемка;
- `downlink` — сброс.
- Timeline черновика.
- Окна видимости цели/станции.
- Синтетическую трассу КА на карте редактора.
- Полигоны заявок из `pcp-request-service`.
- Контуры `SURVEY` из `pcp-mission-planing-service`, если endpoint `/current-plans/missions/{id}/modes` вернул режимы.
- Счетчики:
- всего включений;
- съемок;
- сбросов;
- конфликтов.
### Что умеет делать локально
- Добавить съемку через клик точки на карте.
- Добавить сброс по станции и окну связи.
- Перетаскивать включения по timeline.
- Менять начало/конец включения drag resize.
- Менять время через stepper.
- Дублировать включение.
- Удалять включение.
- Undo/redo.
- Проверять пересечения включений по времени.
- Проверять видимость цели/станции по синтетической орбитальной модели `passes.ts`.
- Показывать контекст других КА в rail, но сейчас `contextLanes.works` пустой.
### Что не умеет сохранять
Кнопка `Применить` вызывает `saveEditedPlan()`, но функция является заглушкой:
```text
Сохранение правок плана во внешний сервис еще не реализовано в backend.
```
Backend endpoint для сохранения отредактированного набора включений отсутствует.
### Execution path редактора
```text
User
|
v
TguEditorTab
|
|-- selectedSpacecraftId / selectedPlan from TguPlanningPage
|
|-- initialize draft
| |
| v
| seedDraft(selectedPlan)
| |
| |-- if selectedPlan has embedded works:
| | `-- use embedded works
| |
| `-- else:
| `-- create fallback downlink work:
| id = {planId}:plan-window
| kind = downlink
| start/end = selected plan start/end
| stationId = selectedPlan.kppId
|
|-- load request context
| |
| v
| fetchRequestsForMap()
| |
| v
| GET /api/pcp-request/v1/requests/map
| |
| v
| pcp-request-service
| |
| v
| requests table
|
|-- load plan modes
| |
| v
| fetchPlanModes(selectedPlan.planId)
| |
| v
| GET /api/current-plans/missions/{selectedPlan.planId}/modes
| |
| v
| pcp-ui-service
| |
| v
| pcp-mission-planing-service
| |
| v
| MissionPlaningService.modesByMission()
| |
| |-- check mission exists
| |-- find modes by mission id
| |-- map SurveyModeEntity -> SurveyModeResponseDTO
| |-- map DropModeEntity -> DropModeResponseDTO
| |
| v
| mission planning DB
|
|-- editor map scene
| |
| |-- groundTrack(spacecraftId) from frontend synthetic orbit
| |-- station markers only if station lat/lon exists
| |-- plan mode SURVEY contourWkt -> planWorks polygons
| `-- request.geometry WKT -> request polygons
|
|-- local draft operations
| |
| |-- addShoot()
| |-- addDownlink()
| |-- updateWork()
| |-- deleteWork()
| |-- duplicateWork()
| |-- undo()
| |-- redo()
| |-- overlaps()
| `-- checkVisibility()
|
`-- apply draft
|
v
saveEditedPlan()
|
v
stub result only, no backend mutation
```
### Важные caveats редактора
- `fetchPlanModes()` использует `selectedPlan.planId` как `missionId` для `/api/current-plans/missions/{missionId}/modes`.
- Если UUID TGU-плана не совпадает с UUID mission в `pcp-mission-planing-service`, endpoint вернет 404, а редактор просто останется без режимов плана.
- Станции строятся из `selectedPlan.kppId`, но координаты станции из backend не загружаются. Поэтому проверка видимости downlink часто возвращает конфликт `Нет координат станции для проверки видимости`.
- Орбитальная модель в редакторе frontend-only и синтетическая: параметры выводятся из hash `spacecraftId`, а не из реальной баллистики/TLE.
## Capability matrix
| Область | Сейчас умеет | Ограничение |
|---|---|---|
| Платформы КА | читать из TGU/NSI, показывать name/NORAD/mission/status | при падении NSI `pcp-tgu-service` может использовать stale cache, если есть |
| TGU-планы | читать persisted plans, строить timeline | UI грузит все планы и фильтрует на клиенте |
| Решение по плану | отправить `ACCEPTED`/`REJECTED` для `WAITING_DECISION` | через test-only controller; нужен active plan + active attempt |
| Карта заявок | загрузить и показать WKT-полигоны заявок | до 5000 заявок, без server-side фильтров kpp/highPriority в UI |
| Слои карты | UI-переключатели есть | кроме заявок, основные слои главной карты не получают данных |
| Редактор включений | локальный draft, drag/resize, add/delete/duplicate, undo/redo, checks | сохранения в backend нет |
| Режимы плана в редакторе | пытается читать `/current-plans/missions/{id}/modes` | возможен mismatch `planId` vs `missionId` |
| Станции | может показывать station markers только при наличии lat/lon | lat/lon станций сейчас не загружаются |
| Тайлы карты | CARTO CDN | внешний network dependency браузера |
## Проверенные исходники
Frontend:
- `services/pcp-tgu-ops-ui/src/App.tsx`
- `services/pcp-tgu-ops-ui/src/api/tguApi.ts`
- `services/pcp-tgu-ops-ui/src/api/requestApi.ts`
- `services/pcp-tgu-ops-ui/src/features/tgu-planning/*`
- `services/pcp-tgu-ops-ui/src/features/tgu-map-2d/*`
- `services/pcp-tgu-ops-ui/src/features/tgu-editor/*`
- `services/pcp-tgu-ops-ui/vite.config.ts`
- `services/pcp-tgu-ops-ui/nginx.conf`
Backend/BFF:
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TguPlanningController.kt`
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TguPlanningService.kt`
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/CurrentPlansController.kt`
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/MissionService.kt`
TGU:
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlatformController.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlanController.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlatformService.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryService.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/TestPlanDecisionService.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/repository/*`
Requests:
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt`
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt`
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/repository/RequestRepository.kt`
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt`
Mission planning:
- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt`
- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt`
- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/*`
- `libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/*`
@@ -0,0 +1,153 @@
# Жизненный цикл статусов плана ТГУ
Справочник по статусам business-plan в расписании ТГУ: когда наступает каждый статус, что он
означает и какой переход его порождает. Источник правды — enum
[`PlannedPlanStatus`](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlannedPlanStatus.kt);
переходы — в `service/IssueDuePlanWorker`, `service/HandlePlanDecisionUseCase`,
`service/CreateDraftUseCase`, `service/CreateChainPlanUseCase`, `dto/PlanResponse`.
Эти же статусы (кроме «зарезервированных», см. ниже) показаны в легенде вкладки «Планы»
([`tguStatus.ts`](../../services/pcp-tgu-ops-ui/src/features/tgu-planning/tguStatus.ts)) и задают
цвет плашек на таймлайне.
## Диаграмма переходов (упрощённо)
```
создать черновик
(нет плана) ───────────────────────────────► DRAFT
│ promote («Применить») / авто-выдача
проекция будущего ····(виртуально)····► PLANNED ──(подошёл срок, авто-выдача)──► ISSUING
│ BPMN-процесс стартовал
WAITING_DECISION
ACCEPTED-решение │ │ REJECTED-решение
▼ ▼
AWAITING_LAYIN REJECTED (цепочка на паузе)
┌────────────────────────────────┼────────────────────────────────┐
событие «заложен» событие «не заложен» событие «не принят»
▼ ▼ ▼
LAID_IN LAYIN_FAILED NOT_ACCEPTED
(база цепочки; конкуренты (срочная перевыдача) (срочная перевыдача)
того же окна → SUPERSEDED)
при сбое старта BPMN из ISSUING ─────────────────────────────────────────► START_AMBIGUOUS
```
## Статусы
### DRAFT — черновик
- **Когда.** Оператор создаёт план через «Создать план» (`CreateChainPlanUseCase`) или «Добавить»/
редактирование (`CreateDraftUseCase`).
- **Что значит.** План в стадии правки. Персистится, но **не** является головой цепочки, **не**
попадает в проекцию/read-путь как запланированный слот и **не** выдаётся авто-воркером.
- **Дальше.** Становится активным только после promote («Применить») — переходит в `ISSUING`.
Удаление черновика (abandon) убирает его и клон-миссию.
- В UI помечен как «ЧЕРНОВИК», виден только при включённом режиме «Черновики».
### PLANNED — проекция будущего (виртуальный)
- **Когда.** Вычисляется на лету `PlanProjectionService`/`PlanResponse.projected()` при чтении
расписания. **Никогда не пишется в БД.**
- **Что значит.** Предполагаемый будущий слот цепочки (следующие ЗРВ) — «как пойдёт цепочка, если
ничего не менять». Не несёт `basePlanId`, стартом цепочки не считается.
- **Дальше.** Когда подходит срок выдачи, по этому слоту авто-воркер материализует реальный план
сразу в `ISSUING` (`materializeIssuingPlan``insertIssuingPlan`).
- В UI виден только при включённом тумблере «Возможные будущие планы».
### ISSUING — идёт выдача
- **Когда.** `IssueDuePlanWorker` начинает выдачу: promote черновика (`prepareDraftPromotion`),
ручная (пере)выдача конкретного плана (`prepareSpecificPlan`) или авто-материализация головы
проекции (`materializeIssuingPlan`).
- **Что значит.** Запущен BPMN-процесс выдачи плана на борт; создан attempt со статусом `STARTING`.
- **Дальше.** Успешный старт процесса → `WAITING_DECISION` (`markStarted`). Сбой старта BPMN →
`START_AMBIGUOUS` (`markAmbiguous`).
### WAITING_DECISION — ожидает решения
- **Когда.** BPMN-процесс выдачи успешно стартовал, attempt → `STARTED` (`markStarted`).
- **Что значит.** План выдан, ждём решения оператора/борта: ACCEPTED либо REJECTED.
- **Дальше.**
- ACCEPTED → `AWAITING_LAYIN` (`acceptPlan`).
- REJECTED → `REJECTED` (`rejectPlan`).
- **Grace-окно.** Если решения нет дольше `waitingDecisionGraceMinutes` после `startTime`, цепочка
«молчаливо» едет дальше: якорь сдвигается на этот план, блокировка снимается, но **статус
остаётся `WAITING_DECISION`** (`tryAdvancePastUnconfirmedPlan`). Запоздалое решение запишется на
attempt и ушедшую вперёд цепочку не тронет. Поэтому в UI кнопки решения гасятся после grace.
### AWAITING_LAYIN — принят, ждёт подтверждения закладки
- **Когда.** Пришло решение ACCEPTED (`acceptPlan`).
- **Что значит.** План принят и ждёт закладки. **ACCEPT больше НЕ делает план базой и не двигает
`chainVersion`** — это разъединено намеренно: после приёмки можно отправить и принять коррекцию
того же окна, поэтому по одному окну может одновременно «лететь» несколько AWAITING_LAYIN/
WAITING_DECISION. Базой и версией цепочки управляет только закладка (`LAID_IN`). На ACCEPT
запускается джоб обновления слотов принятого плана (`acceptedPlanSlotsUpdateJobService.enqueue`).
- **Дальше.** При подтверждении закладки → `LAID_IN` (становится базой). При негативе —
`LAYIN_FAILED`/`NOT_ACCEPTED`. Если базой стала **другая** коррекция того же окна — этот план
гасится в `SUPERSEDED`.
- В легенде это «принятый, ждёт закладки» статус (зелёный). Перевыдача/коррекция в свой слот ещё
возможна, пока до старта больше `layinFreezeMinutes` (после — «точка невозврата», только
fork-forward).
### REJECTED — отклонён
- **Когда.** Пришло решение REJECTED (`rejectPlan`).
- **Что значит.** План признан неправильным. Авто-перевыдача **не** взводится (повторять тот же план
бессмысленно). Цепочка ставится **на паузу** (`pausedAfterReject = true`) — **только если по КА не
осталось других живых кандидатов** (в полёте/приняты, но не заложены): при нескольких выдачах
отказ одной не глушит цепочку, пока жив другой кандидат. На паузе проекция пуста, воркер ничего не
выдаёт. Базовый план не трогается.
- **Дальше.** Оператор вручную создаёт исправленный план; его promote/ручная выдача снимают паузу.
- В UI у КА с паузой рисуется пометка «⏸ на паузе».
### NOT_ACCEPTED — не принят (негативный фидбек)
- **Когда.** Пришло внешнее событие «не принят» по текущему базовому плану в `AWAITING_LAYIN`
(`handleNotAccepted``applyNegativeFeedback`).
- **Что значит.** Терминальный негатив по базовому плану. В отличие от REJECTED — это **технический**
негатив, поэтому запускается **срочная перевыдача** на ближайшую ЗРВ (`reissueOnNearestWindow`).
- Событие, пришедшее не по текущему базовому плану (устаревшее/дубль), игнорируется.
### LAYIN_FAILED — не заложен (негативный фидбек)
- **Когда.** Пришло внешнее событие «не заложен» по текущему базовому плану в `AWAITING_LAYIN`
(`handleLayinFailed``applyNegativeFeedback`).
- **Что значит.** Терминальный негатив: закладка не удалась. Поведение сейчас идентично
`NOT_ACCEPTED`**срочная перевыдача** на ближайшую ЗРВ.
### START_AMBIGUOUS — неопределённый старт
- **Когда.** Из `ISSUING` не удалось достоверно стартовать BPMN-процесс выдачи (исключение в
`startPreparedAttempt``markAmbiguous`); attempt → `AMBIGUOUS`.
- **Что значит.** Неизвестно, ушёл план на борт или нет — требуется ручной разбор оператором.
Опасный статус, в UI выделен пунктиром.
### LAID_IN — заложен (терминальный успех, база цепочки)
- **Когда.** Пришло внешнее событие «план заложен» по плану в `AWAITING_LAYIN`
(`LayinConfirmedConsumer``handleLayinConfirmed`, топик `layin-confirmed`).
- **Что значит.** Подтверждённая закладка — **именно она делает план базой цепочки**
(`state.currentBasePlanId = plan.planId`, `chainVersion += 1`, снимается пауза/срочная
перевыдача). «Точка невозврата»: дальше план меняется только через fork-forward (гарды
`CreateDraftUseCase`, `assertReissuableInSlot`).
- **Supersede конкурентов.** Все ещё не заложенные планы КА (ISSUING/WAITING_DECISION/AWAITING_LAYIN)
с окном, пересекающим окно заложенного, переводятся в `SUPERSEDED` (`supersedeCompetingPlans`).
- Событие по плану не в `AWAITING_LAYIN` (устаревшее/дубль/уже вытеснён) — игнорируется.
- В легенде — зелёный, темнее `AWAITING_LAYIN`.
### SUPERSEDED — вытеснён заложенной коррекцией (терминал)
- **Когда.** По тому же окну КА другой план ушёл в `LAID_IN`, а этот был конкурирующей выдачей/
коррекцией (`supersedeCompetingPlans`).
- **Что значит.** Терминал, цепочку не двигает. Запоздавшее решение по его attempt записывается, но
игнорируется (план уже не in-flight). Базовый план не трогается.
## Зарезервированные / не возникающие сейчас статусы
- **ACCEPTED** — в enum присутствует, но приёмка (`acceptPlan`) переводит план **сразу** в
`AWAITING_LAYIN`, минуя `ACCEPTED`. Фактически это легаси-значение, нигде не присваивается; в
легенду вкладки «Планы» намеренно не вынесено.
## Связанные конфиги (`PlanningProperties`)
- `waitingDecisionGraceMinutes` — сколько ждём решение после старта, прежде чем «молчаливо» ехать
дальше (см. `WAITING_DECISION`).
- `layinFreezeMinutes` — за сколько минут до старта окно выдачи замораживается («точка невозврата»):
перевыдача в свой слот запрещена, только fork-forward.
- `notificationBeforeStartMinutes` — за сколько до старта авто-воркер начинает выдачу головы цепочки.
## См. также
- [PCP_ARCHITECTURE.md](PCP_ARCHITECTURE.md)
- [PCP_TGU_OPS_UI_CAPABILITY_MAP.md](PCP_TGU_OPS_UI_CAPABILITY_MAP.md)
+136
View File
@@ -0,0 +1,136 @@
# GitLab CI/CD for `pcp-ballistics-service`
This document describes the pilot GitLab CI/CD setup for `pcp-ballistics-service`.
It is intentionally scoped to one service so the same structure can be reused later for other services in this monorepo.
## Pipeline behavior
The root pipeline is defined in [.gitlab-ci.yml](/home/emelianovan/WORK/Kotlin/pcp/.gitlab-ci.yml) and includes the service-specific jobs from [.gitlab/ci/pcp-ballistics-service.yml](/home/emelianovan/WORK/Kotlin/pcp/.gitlab/ci/pcp-ballistics-service.yml).
Pipeline creation is limited by `workflow: rules`:
- merge request: create a pipeline only when `pcp-ballistics-service` or its dependencies changed
- push to `dev`: create a pipeline only when `pcp-ballistics-service` or its dependencies changed
- push to `master`: create a pipeline only when `pcp-ballistics-service` or its dependencies changed
No deploy jobs are created for merge requests.
## Jobs
The pilot flow has four stages:
1. `ballistics:test`
Runs `:services:pcp-ballistics-service:test` outside Docker.
Publishes JUnit and HTML/JaCoCo reports.
2. `ballistics:build`
Runs `:services:pcp-ballistics-service:bootJar` outside Docker.
Publishes `build/libs/*.jar` as an artifact.
3. `ballistics:image`
Builds and pushes the Docker image with Kaniko using the jar artifact from the previous stage.
Tags:
- `${CI_COMMIT_SHORT_SHA}`
- `${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}`
4. deploy jobs
- `ballistics:deploy:dev`
- `ballistics:deploy:master`
Both are manual and use Helm:
`helm upgrade --install --wait --atomic --timeout`.
## Branch behavior
### `dev`
Push to `dev`:
- runs test, build and image jobs
- exposes a manual deploy button to the `dev` environment
### `master`
Push to `master`:
- runs test, build and image jobs
- exposes a manual deploy button to the `master` environment
### merge request
Merge request pipeline:
- runs test and build only
- does not create deploy jobs
## `rules:changes` scope
The pilot dependency map for `pcp-ballistics-service` includes:
- `services/pcp-ballistics-service/**/*`
- `libs/pcp-types-lib/**/*`
- `libs/ballistics-lib/**/*`
- `config-repo/pcp-ballistics-service.yaml`
- `config-repo/application*.yaml`
- Gradle root files and wrapper
- CI files
- `charts/pcp-ballistics-service/**/*`
This is the pattern to reuse later for other services: service directory + direct shared libraries + service config + CI/Helm files.
## Helm deploy model
The chart lives in [charts/pcp-ballistics-service](/home/emelianovan/WORK/Kotlin/pcp/charts/pcp-ballistics-service).
The deployment model is kubeconfig-based, not GitLab Agent-based.
This keeps the pilot setup simple and practical. A later migration to GitLab Agent is still possible because deploy logic is already isolated in dedicated jobs.
## Required GitLab CI/CD variables
Registry auth uses standard GitLab registry variables:
- `CI_REGISTRY`
- `CI_REGISTRY_USER`
- `CI_REGISTRY_PASSWORD`
Deploy jobs require:
- `BALLISTICS_KUBE_CONFIG_DEV`
- `BALLISTICS_KUBE_NAMESPACE_DEV`
- `BALLISTICS_CONFIG_SERVER_URI_DEV`
- `BALLISTICS_KUBE_CONTEXT_DEV` optional
- `BALLISTICS_CONFIG_LABEL_DEV` optional, defaults to `master`
- `BALLISTICS_SPRING_PROFILE_DEV` optional, defaults to `dev`
- `BALLISTICS_KUBE_CONFIG_MASTER`
- `BALLISTICS_KUBE_NAMESPACE_MASTER`
- `BALLISTICS_CONFIG_SERVER_URI_MASTER`
- `BALLISTICS_KUBE_CONTEXT_MASTER` optional
- `BALLISTICS_CONFIG_LABEL_MASTER` optional, defaults to `master`
- `BALLISTICS_SPRING_PROFILE_MASTER` optional, defaults to `dev`
`BALLISTICS_KUBE_CONFIG_*` must contain base64-encoded kubeconfig content.
## Config server and Vault
Runtime secrets are not duplicated in GitLab CI.
The Helm chart only passes bootstrap parameters needed by Spring Cloud Config:
- `CONFIG_SERVER_URI`
- `SPRING_PROFILES_ACTIVE`
- `SPRING_CLOUD_CONFIG_LABEL`
- `CONFIG_SERVER_FAIL_FAST`
`pcp-ballistics-service` continues to obtain runtime configuration and secrets through `spring-cloud-config-server` and Vault at runtime.
## How to scale this to other services
To onboard another service later:
1. create a dedicated `.gitlab/ci/<service>.yml`
2. define that service's `rules:changes`
3. add a dedicated Helm chart
4. keep the same stage structure:
`test -> build -> image -> manual deploy`
5. keep config-service/Vault integration at runtime, without copying secrets into GitLab
+105
View File
@@ -0,0 +1,105 @@
# CHECKLIST.md
## Назначение
Этот документ содержит краткие checklists для review/merge, Definition of Done, обязательных тестов и validation.
Пункты ниже применяются по принципу **where relevant**:
- для обычных Kotlin/Spring-изменений достаточно общего checklist;
- для изменений в BPM / Zeebe / Camunda / Kafka / retries / DLQ / compensation / schema evolution дополнительно применяются профильные пункты;
- для критичных процессов глубина проверок и тестов должна быть пропорциональна риску изменения.
---
## Review checklist
### Для всех нетривиальных изменений
Перед merge необходимо подтвердить:
- запрошенное поведение реализовано;
- решение осталось в корректной архитектурной границе;
- существующие публичные контракты не сломаны без явного запроса;
- добавлены или обновлены релевантные тесты;
- не внесено несвязанных правок;
- время на API — naive `LocalDateTime` без зоны; backend штампует `now` через `LocalDateTime.now()`, фронт парсит/форматирует через `utils/utcTime` без `new Date("<строка>")`;
- egress фронта к бэку — naive ISO (`YYYY-MM-DDTHH:mm:ss`, без Z);
- validation выполнен по правилам `AGENTS.md`, либо пропуски явно объяснены.
### Дополнительно для BPM / Kafka / Zeebe / external integration изменений
Перед merge необходимо подтвердить, где это релевантно:
- business key определён и используется консистентно;
- operation id принимается от вызывающей стороны и используется для dedup;
- ошибки классифицированы семантически;
- side effects идемпотентны или явно компенсируются;
- доменные события публикуются через Outbox;
- logs / metrics / traces содержат обязательные поля корреляции;
- тесты покрывают duplicate delivery и retry behavior;
- изменения деплоя и конфигурации трассируемы в VCS.
---
## Definition of Done
Задача считается завершённой только если одновременно выполнено всё ниже, где это релевантно:
### Общее
1. Запрошенное поведение реализовано.
2. Решение осталось в корректной архитектурной границе.
3. Нужные тесты добавлены или обновлены.
4. Нужный validation выполнен, либо невозможность запуска объяснена.
5. Не внесено несвязанных правок.
### Для BPM / Kafka / Zeebe / external integration
6. BPM остаётся только оркестратором, если BPM участвует.
7. Доменные правила остаются внутри сервисной границы.
8. Flows остаются безопасными при at-least-once delivery.
9. Идемпотентность обеспечена.
10. `operation_id` сохранён или добавлен, где это требуется контрактом.
11. Outbox сохранён или использован, где операция меняет состояние и публикует события.
12. Retries ограничены и управляемы.
13. DLQ handling сохранён для критичных consumers.
14. Versioning / backward compatibility сохранены.
15. Observability сохранена или улучшена.
---
## Обязательные тесты для значимых изменений
### Базовый минимум
Как минимум покрывать, где применимо:
- happy path;
- изменённые ветки branching / validation logic;
- regression scenario для bug fix.
### Дополнительно для BPM / Kafka / Zeebe / external integration
Покрывать, где применимо:
- retryable failure с последующим успехом;
- исчерпание retries;
- ветку business error;
- обработку timeout;
- сценарий compensation;
- duplicate delivery / duplicate event handling;
- contract compatibility;
- schema compatibility.
### Для критичных процессов и high-risk изменений
Покрывать пропорционально риску изменения через automated tests и/или явный resilience test plan:
- перезапуск воркера во время выполнения;
- временную недоступность БД;
- временную недоступность внешней зависимости;
- разрыв связности Kafka / BPM;
- сценарий load / lag recovery.
---
## Validation
Следуй разделу `Validation commands` из `AGENTS.md`.
Если для изменения есть module-specific команды, используй минимальный релевантный прогон, а затем более широкий, когда это необходимо.
Если какая-либо команда не была запущена:
- точно укажи, какая именно команда не запускалась;
- объясни почему;
- опиши остаточный риск.
+303
View File
@@ -0,0 +1,303 @@
# PLATFORM_STANDARD.md
## Назначение
Этот документ задаёт платформенные требования для сервисов, работающих в BPM / Kafka / Zeebe-среде.
Документ определяет:
- архитектурные инварианты;
- требования к идемпотентности и надёжности;
- правила BPM / Kafka интеграции;
- требования к observability, compatibility, retry / DLQ, compensation и rollout safety.
Это **внутренний платформенный стандарт**. Некоторые правила ниже являются осознанной инженерной политикой команды и не должны трактоваться как универсальная истина для всей отрасли.
---
## Архитектурные инварианты
- BPM — только оркестратор процесса.
- Доменное состояние и бизнес-правила принадлежат сервису, а не BPM.
- BPM не должен использоваться как master data store для доменных данных.
- BPM может хранить только идентификаторы, ссылки и технические метаданные, необходимые для оркестрации.
- Везде предполагается at-least-once доставка. Повторная доставка — норма, а не исключение.
- Exactly-once не предполагается; корректность достигается через идемпотентность, dedup, Outbox и управляемые retries.
- Каждый обработчик обязан быть идемпотентным.
---
## Правила использования BPM
- BPM использовать только для операторских / регламентных процессов и long-running orchestration.
- Не использовать BPM для высокочастотных near-real-time streaming-потоков.
- Каждый BPM service task должен представлять ровно одну атомарную доменную операцию.
- Логика ожидания, таймаутов, ретраев, эскалаций и компенсаций должна моделироваться явно в BPM, а не быть скрыта внутри сервиса.
- BPMN flow не должен становиться местом владения бизнес-логикой.
---
## Camunda 8 / Zeebe
### Граница интеграции
- BPM-to-service-task интеграция должна идти через Zeebe gRPC Job Worker.
- Для нашей платформы использование REST connector для service task считается **не рекомендуемым и по умолчанию запрещённым**, если нет отдельного архитектурного согласования.
- Нельзя изменять `jobType`, пока существуют активные инстансы процесса.
### Обязательные поля корреляции
Каждый service task обязан принимать / обрабатывать:
- `businessKey`
- `operationId`
- `correlationId`
- `processInstanceKey`
- `traceId` или `traceparent`
### Контракт воркера
Для каждого Zeebe worker:
- читать входные данные только из process / job variables;
- валидировать обязательные поля корреляции до начала обработки;
- проверять dedup store по `operationId` до выполнения side effects;
- возвращать семантические результаты:
- `SUCCESS` -> `CompleteJob`
- `RETRYABLE_ERROR` -> `FailJob` с retry / backoff
- `BUSINESS_ERROR` -> `ThrowError`
- `FATAL_ERROR` -> ветка инцидента или обработка через исчерпание retries
- никогда не генерировать `operationId` внутри сервиса для входящих оркестрируемых вызовов;
- вызывать `CompleteJob` только после commit доменной транзакции.
---
## Идемпотентность
Все операции, вызываемые из BPM, и все Kafka consumers обязаны быть идемпотентными.
Обязательные правила:
- `operationId` должен приходить от вызывающей стороны / оркестратора;
- dedup storage должен обеспечивать atomic check-and-write semantics;
- dedup storage должен защищать от race conditions;
- повторный вызов с тем же `operationId` должен возвращать тот же семантический результат и не должен повторять side effects;
- для записей dedup должен быть документирован TTL в соответствии с бизнес-окном возможного повтора;
- non-atomic реализация идемпотентности запрещена.
Дополнительное обязательное правило:
- повторный вызов с тем же `operationId`, но с другим `businessKey` или семантически иным payload / параметрами, считается конфликтом контракта, а не валидным retry; такой случай должен приводить к явной ошибке / инциденту и быть достаточно залогирован для расследования.
Рекомендуемая реализация:
- таблица в доменной БД, например `idempotency_keys(operation_id, business_key, status, result_hash, created_at)`.
---
## Порядок транзакции и публикации событий
Когда операция изменяет доменное состояние и публикует события:
1. Выполнить доменную операцию.
2. Записать событие(я) в outbox в рамках той же транзакции.
3. Сделать commit транзакции.
4. Только после этого подтверждать BPM (`CompleteJob`) или внешний success.
Публикация событий вне Outbox запрещена.
Это правило одинаково применяется к:
- синхронным сервисным операциям;
- Zeebe workers;
- Kafka consumers, которые меняют состояние и публикуют новые события.
---
## Process variables
- Process variables должны быть минимальными.
- Хранить только ID, ссылки, routing data и небольшие технические метаданные.
- Не хранить файлы, большие JSON payload, большие массивы в process variables.
- Большие payload должны храниться в доменной БД или object storage; в process variables должны храниться только ссылки.
- Process variables должны быть версионированы и документированы.
- Для long-running процессов изменения набора или формы variables должны оставаться совместимыми с уже запущенными инстансами, либо сопровождаться явной migration strategy.
- Для нашей платформы безопасным внутренним ориентиром считается бюджет **50100 KB на один process instance**. Это не технический максимум платформы, а engineering guardrail для управляемости и надёжности процессов.
---
## Семантика результатов
Каждая операция должна возвращать семантическую классификацию результата, а не только transport status codes.
Допустимые классы результатов:
- `SUCCESS`
- `RETRYABLE_ERROR`
- `BUSINESS_ERROR`
- `FATAL_ERROR`
Каждая long-running операция должна иметь:
- явно заданный SLA / timeout;
- async processing model, где это нужно;
- status lookup по `operationId`;
- определённое поведение cancel, если отмена поддерживается.
---
## Kafka / event-driven
### Базовые правила
- Базовое предположение по доставке — at-least-once.
- Consumers должны быть идемпотентными по `event_id` и / или `operation_id`.
- Нельзя размещать бизнес-логику напрямую в consumer / listener классах.
- Consumers / listeners должны делегировать в application / domain services.
- Сохранять текущую retry / DLQ-стратегию, если явно не запрошено её изменение.
### Семантика `event_id` и `operation_id`
- `event_id` используется для dedup повторной доставки **одного и того же опубликованного события**.
- `operation_id` используется для dedup повторного выполнения **одной и той же бизнес-операции**, если этот идентификатор протягивается через события и обработчики.
- Если в сообщении присутствуют оба идентификатора, обработчик обязан явно понимать и документировать, какой из них используется для какой семантики dedup.
### Контракты событий
Требования ниже относятся прежде всего к **domain events**. Для технических, retry, DLQ, compacted state и других служебных топиков может использоваться отдельная документированная конвенция.
Для domain events:
- именование топиков должно следовать шаблону `<domain>.<entity>.<event>.v<major>`;
- partition key должен сохранять порядок событий по одной сущности через `business_key`;
- каждое событие должно содержать как минимум:
- `event_id`
- `event_type`
- `event_version`
- `occurred_at`
- `producer`
- `business_key`
- `correlation_id`
- `trace_id`, если доступен
- `process_instance_id`, если применимо
- предпочтительны Avro / Protobuf со Schema Registry;
- строго валидируемая JSON Schema допустима только если это согласовано платформой.
### Что нельзя менять без явного запроса
- topic names
- event schema
- key semantics
- partition assumptions
- serialization format
- retry topology
- DLQ routing rules
---
## Schema evolution и совместимость
- Breaking changes требуют новой major-версии.
- Добавление optional fields с default значениями допустимо.
- Удаление или переименование полей требует новой major-версии.
- По умолчанию consumers должны оставаться совместимыми как минимум с текущей схемой и согласованной политикой совместимости платформы. Если для конкретного домена зафиксировано правило поддержки текущей и предыдущей minor-версии, оно должно быть явно указано в контракте этого домена.
Для BPM, messaging и public APIs:
- сохранять backward compatibility, если явно не запрошен breaking change;
- учитывать rolling deployment compatibility;
- учитывать coexistence старых и новых consumers;
- избегать изменений, требующих lockstep deployment, если это явно не запрошено.
---
## Retry / DLQ
Retries должны быть управляемыми и конечными.
Рекомендуемая политика:
- `RETRYABLE_ERROR`: ограниченные retries с exponential backoff и jitter
- `BUSINESS_ERROR`: никаких слепых retries; отправка в operator handling / parking lot / DLQ
- `FATAL_ERROR`: никаких слепых retries; создание инцидента и маршрутизация в DLQ, где применимо
Обязательные правила:
- бесконечные retries запрещены;
- отключение DLQ для критичных consumer flows запрещено;
- replay из DLQ без установленной причины запрещён;
- Zeebe retries и Kafka retries должны рассматриваться как два независимых контура управления.
---
## Compensation
Если операция имеет side effects, должно быть явно определено одно из следующего:
- compensating operation;
- явное указание, что компенсация невозможна;
- ветка operator / manual remediation.
Компенсация должна быть:
- идемпотентной;
- детерминированной;
- семантически классифицированной;
- залогированной с `businessKey`, `operationId`, причиной и результатом.
---
## Observability
Структурированные JSON-логи обязательны.
Каждый log / event / span должен содержать, где применимо:
- `correlation_id`
- `trace_id`
- `process_instance_id`
- `business_key`
- `operation_id`
- `service_name`
- `env`
Обязательная телеметрия:
- latency (`p50/p95/p99`)
- error rate по семантическим классам
- retry rate / attempts histogram
- active process instances / stuck instances
- consumer lag / DLQ rate
- compensation rate
Трассировка:
- OpenTelemetry обязателен
- каждый шаг процесса должен создавать span
- span attributes должны содержать `process_instance_id`, `business_key`, `operation_id`, `step_name`
Если меняется Kafka / BPM / external integration logic:
- сохранить или добавить correlation identifiers;
- сохранить или добавить `operation_id`, где это релевантно;
- логировать достаточно для дебага retries, duplicates и compensations;
- никогда не логировать секреты или sensitive payloads.
---
## Backpressure и runtime load
- Воркеры должны явно ограничивать `maxJobsActive` и concurrency.
- Нельзя использовать рост timeout или retry как механизм масштабирования.
- Backpressure должен управляться через масштабирование воркеров и управление пропускной способностью.
- Предположения о throughput воркеров должны быть документированы.
- CPU / RAM saturation должна наблюдаться и иметь алерты.
---
## Безопасность и аудит
- Сервисы должны поддерживать JWT / OAuth2 там, где это применимо.
- Административные операции должны быть защищены role-based или attribute-based access control.
- Секреты нельзя хранить в репозитории.
- Административные действия, такие как retry / cancel / compensate / migrate, должны быть аудитируемыми.
---
## CI / CD и production safety
Минимально обязательные гейты **для изменений, где это релевантно**:
- unit tests
- coverage threshold
- static analysis
- API compatibility checks
- schema compatibility checks
- BPM + worker integration tests
- resilience tests для поведения timeout / retry / DLQ
Правила деплоя:
- деплой BPMN только через pipeline;
- деплой воркеров только через pipeline;
- ручной деплой BPMN в production запрещён;
- критичные изменения retry / timeout / jobType не должны вноситься только через UI без трассируемости в VCS.
+30
View File
@@ -0,0 +1,30 @@
# Следующая задача: маршруты, сбросы и перенос включений между планами
> Статус: запланировано (дизайн ещё не начат). Это фиксация намерения, не финальный план.
Следующий блок работ — три связанные темы вокруг содержимого плана (включений) и
их съёмочно-сбросовой обвязки:
## 1. Построение маршрутов
Довести построение маршрутов наблюдения по плану. Сейчас маршруты порождаются в
потоке `createSatelliteMission` (BPMN), где `planId == missionId`; результат
ложится в БД mission-planing. Уточнить: когда и по какому триггеру строятся
маршруты для нового/перевыданного плана, и как они привязаны к включениям.
## 2. Формирование сбросов
Формирование сбросов (`DropMode`) поверх съёмочных включений (`SurveyMode`):
расчёт окон сброса, привязка к ЗРВ/КПП, согласование времени сброса со временем
съёмки. Внутренние времена — naive `LocalDateTime` (см. сквозную модель без зон).
## 3. Перенос включений из плана в план
При перевыдаче/версионировании плана переносить уже сформированные включения
(съёмки + сбросы) из предыдущего плана в новый — чтобы не терять набранное
содержимое цепочки. Определить, что переносится как есть, что пересчитывается,
и как это стыкуется с двумя осями версионирования (цепочка ТГУ + контент миссии).
## Контекст / связанные места
- Маршруты и их сохранение: `createSatelliteMission`, `planId == missionId`.
- Сбросы/съёмки: `pcp-mission-planing-service``SurveyMode`/`DropMode`,
`MissionPlaningService`.
- Версионирование/перевыдача плана — две оси (цепочка ТГУ + контент миссии).
- Время — сквозной naive `LocalDateTime` без зоны (после отката UTC-слоя).
+208
View File
@@ -0,0 +1,208 @@
<!-- /autoplan restore point: /home/dasolovyev/.gstack/projects/missionis-observatio-terrae/feat-tgu-slots-filters-autoplan-restore-20260616-165024.md -->
# Эргономика выбора слотов: выбор «полосами» + правило выполнимости брони
## Проблема (как сформулировал оператор)
Сделана работа по сортировке, подсветке брони, показу забронированных, расчёту %
покрытия. Но **сам выбор слотов под бронь по-прежнему неудобен**: сейчас оператор
отмечает слоты галочками поштучно (или Shift+клик диапазоном, или «все видимые /
все сутки»). На сотнях-тысячах слотов это утомительно. Плюс выбор «слепой» — он не
знает, можно ли вообще физически отснять выбранный набор (манёвренность КА).
## Решённые premise (premise-гейт, 2026-06-16, скорректировано после CEO-ревью)
1. **Ограничение брони через смену крена реально существует** (агилити КА).
2. **КОРРЕКЦИЯ:** модель манёвренности УЖЕ есть в slots-service, по-КА из каталога
(`mmi`, `maxSurveyDuration`, `maxChainLength`, `ROLL_EPSILON=0.01` — см. ниже).
Ручная бронь (`bookReq`) её не проверяет, но coverage-solver и `buildSurveyChains`
её соблюдают. Поэтому **не хардкодим физику на фронте** — проверка выполнимости
будет **бэкенд validate-эндпоинтом**, переиспользующим эти по-КА параметры.
3. **Iteration 1 = только UX выбора полосами** (frontend, без правила). Проверка
выполнимости — **iteration 2** (бэкенд validate-эндпоинт + проводка на фронте).
## Что уже есть в бэкенде (источник правды для it.2)
- Параметры по-КА из каталога ([SatelliteCatalogClient.kt:116](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatelliteCatalogClient.kt#L116),
[AbstractSatellite.kt:25](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/satellite/AbstractSatellite.kt#L25)):
`mmi`=10с (мин. интервал маневра, тот же крен), `maxSurveyDuration`=300с,
`maxChainLength`=30 слотов, `ROLL_EPSILON`=0.01.
- `buildSurveyChains` / `canMergeInTime` / `sameRoll` ([SlotService.kt:2151](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt#L2151))
— склейка цепочек по-КА (для отрисовки), `OccupiedIntervalPrefilter` — mmi/maxSurveyDuration.
- **Несоответствия исходного черновика** (закрыты решением «правило за бэком»): фронтовые
150с ≠ бэкендовые 300с; фронтовый ε=0.1° ≠ бэкендовый 0.01; «30с на смену крена» —
в бэке отдельной такой константы НЕТ (склейка только при том же крене). Эти числа
сверяются с каталогом/баллистиками в it.2, НЕ хардкодятся.
## Что уже есть (карта на существующий код)
- Выбор: `selectedKeys: Set<slotKey>` в [TguSlotsTab.tsx](../../services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx).
Поштучный чек, Shift+клик-диапазон ([slotSelection.ts](../../services/pcp-tgu-ui-service/src/features/tgu-slots/slotSelection.ts) `selectionAfterCheck`),
«выбрать видимые», «все сутки», «снять выбор».
- Слот несёт `roll`, `revolution`, `revolutionSign`, `cycle`, `satelliteId`, `slotNumber`, `tn`, `tk`, `state`.
- Чистые модули с юнит-тестами: slotFilters.ts, slotSelection.ts, coverage.ts (+ .test.ts) — образец стиля.
- Бронь: `book()` → подтверждение → `performBook` (чанк 200), снятие аналогично.
- Бэкенд бронирует без валидации манёвренности ([SlotService.kt:2500](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt#L2500)). В it.1 не трогаем.
## Решение — Iteration 1 (только UX выбора полосами, frontend) — ФИНАЛ после ревью
### Модуль 1: `slotStrips.ts` (чистая логика + юнит-тесты)
- `groupStrips(slots, keyOf): Map<slotKey, StripId>` — максимальные same-roll цепочки
**по-КА**. `keyOf` — аксессор ключа слота (как `keyAt` в `selectionAfterCheck`), чтобы
не дублировать формулу `${satelliteId}:${slotNumber}:${cycle}`.
- **Детерминизм:** внутри функции тотальный порядок — сортировка по
`(satelliteId, parseLocal(tn), parseLocal(tk), slotNumber, cycle, slotKey)`. Результат
не зависит от порядка входного массива.
- **ε крена = `ROLL_EPSILON = 0.01`** (именованная константа, как в бэкенде; НЕ 0.1).
Сравнение — **с якорным креном полосы** (`abs(roll anchorRoll) ≤ 0.01`), НЕ с
предыдущим слотом (иначе дрейф крена 0.00→0.009→0.018 ошибочно склеит). Совпадает с
бэкендовым `sameRoll(roll, chainRoll)`.
- **StripId стабилен и читаем:** `strip:${slotKey первого члена}` (не счётчик — счётчик
зависит от порядка обхода КА).
- Краевые: один слот → своя полоса; два КА вперемешку по времени НЕ склеиваются;
`tn` не парсится (NaN) → синглтон без падения; ровно на границе ε (`==0.01`) → та же полоса.
### Модуль 2: выбор «полосами» (UX в TguSlotsTab) — решения ревью
- **Жест: чип в строке `полоса ×N`** (N = число видимых членов полосы). НЕ Alt+клик
(необнаружим, конфликт с чек/Shift), НЕ клик по карте как основной. Чип появляется
только при N≥2 и если есть хоть один выбираемый (не-BUSY) член.
- **Identity ≠ selection target:**
- `stripIdByKey = useMemo(groupStrips(displaySlots, slotKey), [displaySlots])` — id по
СТАБИЛЬНОМУ набору (не filteredSlots/sortedSlots — иначе фильтр-дырка ложно склеит/разорвёт).
- Видимые выбираемые члены полосы берём из `filteredSlots``!slotIsBusy` (по образцу
`selectableVisible`).
- **Поведение чипа = `toggleAllVisible`, но в рамках одной полосы:** если ВСЕ видимые
выбираемые члены уже выбраны → снять их; иначе добавить. После — `anchorIndexRef.current = null`.
Без нового state (`selectedStrips` не заводим — рассинхрон с `selectedKeys`).
- **Группировка визуально по StripId, sort-aware:** в сортировке «время» — левый рельс
(border-left, цвет по `StripId`, приглушённый) на смежных строках полосы. В «крен»/«вклад»
слоты полосы НЕ подряд → рельс НЕ рисуем; подсветка членов полосы по StripId при наведении.
Фон строки оставляем за состоянием (active/selected); статус-ячейки не перекрашиваем.
- **Карта it.1:** оставляем существующую подсветку выбранных слотов (`markedObjectIds` уже
ведётся от `selectedKeys`). Отдельную раскраску полос на карте и «выбрать полосу из
`SlotPickMenu`» — НЕ делаем в it.1 (расширяет scope на модель/рисовалку карты).
- **CSS/таблица:** обновить `colCount`; выравнивание ячеек — классами, не сдвигом
`nth-child`; чип держать в пределах `ROW_HEIGHT = 34`.
### Матрица состояний строки (зафиксировать перед реализацией)
| Состояние | Источник | Визуал |
|---|---|---|
| active (фокус по карте) | `activeSlotId` | подсветка строки (как есть) |
| selected | `selectedKeys.has(key)` | чек отмечен |
| strip-selected | все видимые выбираемые члены полосы выбраны | чип в активном виде |
| partial | часть членов полосы выбрана | чип в нейтральном виде |
| disabled chip | N<2 или 0 выбираемых членов | чипа нет |
| booked/occupied | как есть (бейдж) | не меняется; в полосу входит визуально, в выбор — нет |
## Реализовано (it.1, 2026-06-17 — пересмотрено)
ВАЖНО: первая версия it.1 (авто-чип «полоса ×N» по равному крену) оказалась не той фичей и
переделана. На реальных данных полоса по «подряд по времени + равный крен» не формировалась
(в один момент идёт «веер» кренов с шагом ~3°, равный крен повторяется не подряд, а вдоль трассы
через каденцию). Оператор хотел не авто-магию, а ручной выбор полосы мышью «от первого к последнему»
с запретом выйти за полосу.
Переопределение полосы (согласовано с оператором):
- **полоса = один КА + один крен (допуск `ROLL_EPSILON`) + подряд по времени (без разрыва каденции);**
- **виток/цикл в границу НЕ входят** — у экватора номер витка меняется, а слоты остаются в одной полосе.
Сделано:
- `slotStrips.ts`: `groupStrips(slots, keyOf) → { idByKey, membersOf }`. Группировка: по КА →
кластеризация по крену (`ROLL_EPSILON=0.5`, разрыв > ε начинает кластер) → внутри кластера деление
по непрерывности времени (`CADENCE_GAP_FACTOR=1.5` от медианной каденции). `PolosaId =
polosa:${ключ первого по времени члена}`; `membersOf` — члены в порядке времени.
- `slotSelection.ts`: вместо `toggleStripSelection` — `selectStripRange(current, members, fromKey,
toKey)` (чистая: добавляет членов полосы между двумя кликами включительно, без мутации).
- `TguSlotsTab.tsx`: убран чип/hover/`stripInfo`. Жест: обычный клик ставит якорь; Shift+клик по
слоту той же полосы выделяет членов между якорем и кликом (BUSY исключаются); Shift+клик по слоту
другой полосы/КА — выбор не меняется + подсказка «только в пределах одной полосы». Якорь — состояние
(`anchorKey`), строки полосы якоря подсвечены рельсом (оттенок по `PolosaId`); сбрасывается при
смене заявки/сортировки/фильтров/расчёта. Подсказка жеста в selbar.
- `tgu-planning.css`: удалены стили чипа/hover; рельс `tgu-slots-row--strip` переиспользован под
полосу якоря, добавлен `is-strip-anchor`.
- Калибровка `ROLL_EPSILON=0.5` / `CADENCE_GAP_FACTOR=1.5` — по скрину данных; при ручном прогоне
возможна подстройка по фактическим `roll`/`tn`.
- Бэкенд/DTO не тронуты. Правило выполнимости (манёвренность КА) по-прежнему it.2.
Доработки (2026-06-17):
- **Shift+клик ближе к якорю «сжимает» выделение.** `selectStripRange` теперь задаёт выбор полосы
РОВНО диапазоном [from..to]: члены полосы вне диапазона снимаются, выбор в других полосах сохраняется.
Якорь при Shift+клике не двигается (остаётся первым кликом) — можно тянуть конец туда-обратно.
- **Жест выбора полосы мышью на карте (Shift+drag).** Слоты на карте уже кликабельные полигоны
(`slot:${key}` в spatial-index). Карта на жесте отдаёт наверх ключи слотов под курсором
(`onStripAnchor`/`onStripDrag`/`onStripCommit`), а полосу/диапазон считает `TguSlotsTab`
(`pickStripAnchor` — самый ранний по времени слот под курсором = начало; `pickStripEnd` — самый
поздний член ТОЙ ЖЕ полосы под курсором = конец; за границу полосы выделение не уходит). Обычный
drag = панорама, Shift+drag по слоту = выбор полосы (без отдельной кнопки-режима). Подсветка
диапазона живая — через `markedObjectIds` (SVG-оверлей `is-marked`), отдельный слой превью не нужен.
## Решение — Iteration 2 (проверка выполнимости, отдельно)
- Бэкенд: read-only `POST /api/slots/booking/validate` (набор слотов → violations),
переиспользует по-КА `mmi`/`maxSurveyDuration`/`maxChainLength`/`ROLL_EPSILON` из каталога.
Сверить точную семантику (300 vs «150 на полосу», нужна ли отдельная 30с-граница) с
баллистиками. Опционально позже — валидация и в самом `bookReq`.
- Фронт: гонять validate при выборе/перед бронью, показывать violations, решить
блокировать кнопку или предупреждать.
## NOT in scope (it.1)
- Любая физика выполнимости на фронте (перенесено в it.2, за бэкенд).
- Переработка автоподбора покрытия (GREEDY/CONTINUOUS).
- Изменение модели хранения броней.
## Тест-план (it.1) — финал
- `slotStrips.test.ts` (groupStrips): мультиполоса в одном КА; два КА вперемешку по времени
(не склеиваются); один слот → своя полоса; граница ε (`diff==0.01` склеивает, `0.0101` рвёт);
дрейф крена 0.00/0.009/0.018 (с якорным сравнением → две полосы); устойчивость к порядку
входа (равенство РАЗБИЕНИЯ при перемешивании); tie-break по равному `tn`; NaN `tn` → синглтон
без падения; пустой вход → пустая Map.
- **Чистый хелпер выбора полосы** (выделить из компонента, как slotSelection): скрытые члены
не выбираются; BUSY исключён; «все видимые выбраны → снять, иначе добавить»; нет мутации
входного `selectedKeys`.
- Ручная проверка в vite dev: чип `полоса ×N`, рельс в сортировке «время», подсветка по
StripId в «крен»/«вклад», согласованность с Shift+клик/чек/«все сутки», счётчик «Выбрано».
## Смежная находка (вне scope, флаг оператору)
`anchorIndexRef` (якорь Shift-выбора) сбрасывается при смене сортировки, но НЕ при смене
фильтров (статус/КА/ветвь/сутки), хотя они тоже меняют порядок строк → Shift-диапазон после
фильтрации может выбрать не то. Существующий латентный баг, рядом с этой задачей. Дёшево
починить (сбрасывать якорь и при смене фильтров) — включить в it.1 или отдельным фиксом.
---
## Review artifacts (/autoplan, 2026-06-16)
### CEO consensus
| # | Вопрос | Claude | Codex | Консенсус |
|---|--------|--------|-------|-----------|
| 1 | Та ли проблема | да (UX выбора) | да | CONFIRMED |
| 2 | Фронт-физика — регрет? | да, критично | да, критично | CONFIRMED → правило за бэк |
| 3 | Бэкенд уже имеет модель? | да (нашёл chain-логику) | — | подтверждено в коде |
| 4 | Калибровка scope | разбить | разбить | CONFIRMED → it.1 только UX |
### Design consensus
| Тема | Claude | Codex | Консенсус |
|------|--------|-------|-----------|
| Жест | чип в строке | чип в строке | CONFIRMED |
| Grouping vs sort | sort-aware по StripId | sort-aware по StripId | CONFIRMED |
| Скрытые члены | только видимые | только видимые | CONFIRMED |
| Карта | вторично/отложить | вторично/отложить | CONFIRMED |
| Состояния | матрица | матрица | CONFIRMED |
### Eng consensus
| Тема | Claude | Codex | Консенсус |
|------|--------|-------|-----------|
| StripId над displaySlots | да | да | CONFIRMED |
| Identity ≠ selection target | да | да | CONFIRMED |
| Memo на [displaySlots] | да | да | CONFIRMED |
| ε: якорь vs предыдущий | оставил открытым | якорь (анти-дрейф) | DISAGREE → решено: якорь |
| Бэкенд-изменения в it.1 | нет | нет | CONFIRMED |
| Сброс якоря на фильтрах | — | да (смежный баг) | флаг оператору |
### Decision Audit Trail
| # | Фаза | Решение | Класс | Принцип | Обоснование |
|---|------|---------|-------|---------|-------------|
| 1 | premise | Правило выполнимости — за бэкенд (не фронт-хардкод) | User Challenge | — | оба ревьюера + бэкенд уже имеет по-КА модель из каталога |
| 2 | premise | it.1 = только UX выбора; правило → it.2 | User Challenge | P2/P3 | развязать безопасный win от спорной физики |
| 3 | design | Жест = чип `полоса ×N` | Taste | P5 (явное) | обнаружим, не конфликтует с чек/Shift |
| 4 | design | Grouping sort-aware (рельс в «время», подсветка иначе) | Mechanical | P1 | зебра по строкам врёт вне time-сортировки |
| 5 | design | Чип выбирает только видимые не-BUSY члены | Mechanical | P1 | не выбирать невидимое (доверие счётчику) |
| 6 | eng | StripId над `displaySlots`, memo на `[displaySlots]` | Mechanical | P3 | стабильность id + перф на тысячах строк |
| 7 | eng | ε-сравнение с якорным креном полосы | Mechanical | P1 | анти-дрейф, совпадает с бэкендовым sameRoll |
| 8 | eng | Без бэкенд/DTO-изменений в it.1 | Mechanical | P4 | всё нужное уже в SlotDTO |
| 9 | eng | Карта — без раскраски полос в it.1 | Mechanical | P3/P5 | не расширять scope на модель карты |
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Карта работ КА — Mission Ops</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--bg-0: oklch(0.18 0.012 165);
--bg-1: oklch(0.215 0.013 165);
--bg-2: oklch(0.255 0.014 165);
--bg-3: oklch(0.30 0.015 165);
--line: oklch(0.34 0.014 165);
--line-soft: oklch(0.28 0.013 165);
--grid: oklch(0.26 0.012 165);
--ink: oklch(0.95 0.01 150);
--ink-1: oklch(0.80 0.012 160);
--ink-2: oklch(0.64 0.012 165);
--ink-3: oklch(0.50 0.012 165);
--t-optical: oklch(0.80 0.14 158);
--t-radar: oklch(0.82 0.13 82);
--t-combo: oklch(0.74 0.15 6);
--t-optical-dim: oklch(0.80 0.14 158 / 0.16);
--t-radar-dim: oklch(0.82 0.13 82 / 0.16);
--t-combo-dim: oklch(0.74 0.15 6 / 0.16);
--accent: oklch(0.82 0.13 200);
--warn: oklch(0.80 0.15 55);
--now: oklch(0.86 0.16 25);
--radius: 7px;
--mono: "IBM Plex Mono", ui-monospace, monospace;
--sans: "IBM Plex Sans", system-ui, sans-serif;
--shadow: 0 8px 30px oklch(0.12 0.01 165 / 0.55);
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font-family: var(--sans);
background: var(--bg-0);
color: var(--ink);
font-size: 13px;
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
#root { height: 100vh; }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--bg-0); }
::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: 6px; border: 2px solid var(--bg-0); }
::-webkit-scrollbar-thumb:hover { background: var(--line); }
::-webkit-scrollbar-corner { background: var(--bg-0); }
.mono { font-family: var(--mono); }
.tnum { font-variant-numeric: tabular-nums; }
button { font-family: inherit; color: inherit; cursor: pointer; }
a { -webkit-tap-highlight-color: transparent; }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="data.jsx"></script>
<script src="world.js"></script>
<script src="basemap.js"></script>
<script type="text/babel" src="util.jsx"></script>
<script type="text/babel" src="orbit.jsx"></script>
<script type="text/babel" src="tabs.jsx"></script>
<script type="text/babel" src="mapview.jsx"></script>
<script type="text/babel" src="mapdetails.jsx"></script>
<script type="text/babel" src="mapapp.jsx"></script>
</body>
</html>
+227
View File
@@ -0,0 +1,227 @@
// Корневой компонент: состояние, фильтры, масштаб, тулбар, сборка строк.
const { useState, useMemo, useEffect, useRef } = React;
// Пресеты масштаба: имя видимое окно в мс (центрируется на «сейчас» при выборе).
const HOUR = D.HOUR, DAY = D.DAY;
const SCALES = [
{ id: "week", label: "Неделя", span: 7 * DAY, minPx: 64 },
{ id: "2week", label: "2 недели", span: 14 * DAY, minPx: 70 },
{ id: "month", label: "Месяц", span: 30 * DAY, minPx: 80 },
{ id: "range", label: "Весь диапазон", span: D.RANGE_END - D.RANGE_START, minPx: 90 },
];
function App() {
// Видимость групп / скрытые КА.
const [groupShown, setGroupShown] = useState(() => {
const o = {}; D.GROUP_DEFS.forEach((g) => (o[g.id] = true)); return o;
});
const [scHidden, setScHidden] = useState(() => new Set());
const [typeFilter, setTypeFilter] = useState({ optical: true, radar: true, combo: true });
const [search, setSearch] = useState("");
const [scaleId, setScaleId] = useState("week");
const [selectedIv, setSelectedIv] = useState(() => {
const p = new URLSearchParams(location.search).get("plan");
return p ? p : null;
});
const [selectedSc, setSelectedSc] = useState(null);
const [timelineCollapsed, setTimelineCollapsed] = useState(() => new Set());
const [scrollSignal, setScrollSignal] = useState(0);
const [winCenter, setWinCenter] = useState(D.NOW);
const byId = useMemo(() => {
const m = {}; D.plans.forEach((iv) => (m[iv.id] = iv)); return m;
}, []);
const scById = useMemo(() => {
const m = {}; D.spacecraft.forEach((sc) => (m[sc.id] = sc)); return m;
}, []);
// Масштаб px/ms (умещаем span в условные 1100px рабочей ширины).
const scale = SCALES.find((s) => s.id === scaleId);
const VIEW_W = 1180;
const pxPerMs = VIEW_W / scale.span;
const rangeStart = D.RANGE_START;
const rangeEnd = D.RANGE_END;
// Фильтрация КА.
const scByGroup = useMemo(() => {
const m = {};
D.spacecraft.forEach((sc) => { (m[sc.groupId] = m[sc.groupId] || []).push(sc); });
return m;
}, []);
const q = search.trim().toLowerCase();
const matchesSearch = (sc) => !q || sc.name.toLowerCase().includes(q) || sc.short.toLowerCase().includes(q);
// Счётчики для легенды.
const counts = useMemo(() => {
const byType = {};
D.spacecraft.forEach((sc) => { byType[sc.type] = (byType[sc.type] || 0) + 1; });
return { byType };
}, []);
// Интервалы по КА (с учётом типового фильтра не трогаем фильтр по типу скрывает строки целиком).
const ivBySc = useMemo(() => {
const m = {};
D.plans.forEach((iv) => { (m[iv.scId] = m[iv.scId] || []).push(iv); });
return m;
}, []);
// Сборка строк таймлайна.
const rows = useMemo(() => {
const out = [];
for (const g of D.GROUP_DEFS) {
if (!groupShown[g.id]) continue;
const list = (scByGroup[g.id] || [])
.filter((sc) => typeFilter[sc.type])
.filter((sc) => !scHidden.has(sc.id))
.filter(matchesSearch);
if (list.length === 0) continue;
out.push({ kind: "group", group: g, count: list.length });
if (timelineCollapsed.has(g.id)) continue;
for (const sc of list) {
out.push({ kind: "sc", sc, ivs: ivBySc[sc.id] || [] });
}
}
return out;
}, [groupShown, scHidden, typeFilter, q, timelineCollapsed]);
// Цепочка родословной выбранного интервала.
const lineageSet = useMemo(() => {
if (!selectedIv) return null;
const lin = lineageOf(selectedIv, byId);
return new Set(lin.chain);
}, [selectedIv]);
const selIv = selectedIv ? byId[selectedIv] : null;
const selSc = selIv ? scById[selIv.scId] : null;
const selGroup = selSc ? D.GROUP_DEFS.find((g) => g.id === selSc.groupId) : null;
const toggleGroup = (id) => setGroupShown((s) => ({ ...s, [id]: !s[id] }));
const toggleSc = (id) => setScHidden((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
const toggleCollapse = (id) => setTimelineCollapsed((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
const onPickSc = (id) => {
setSelectedSc(id);
// прокрутка к первому интервалу этого КА
const ivs = ivBySc[id] || [];
if (ivs.length) { setSelectedIv(null); }
};
// Статистика по видимым.
const stats = useMemo(() => {
let sc = 0, iv = 0, ov = 0;
rows.forEach((r) => {
if (r.kind === "sc") {
sc++;
iv += r.ivs.length;
ov += overlapSegments(r.ivs).length;
}
});
return { sc, iv, ov };
}, [rows]);
const allOn = D.GROUP_DEFS.every((g) => groupShown[g.id]) && scHidden.size === 0;
const showAll = () => { const o = {}; D.GROUP_DEFS.forEach((g) => (o[g.id] = true)); setGroupShown(o); setScHidden(new Set()); };
return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<TopTabs active="plans" mapHref="Map.html" />
<div style={{ flex: 1, display: "grid", gridTemplateColumns: `268px 1fr ${selIv ? "340px" : "0px"}`, minHeight: 0, transition: "grid-template-columns .18s" }}>
<Sidebar
groups={D.GROUP_DEFS} scByGroup={scByGroup}
typeFilter={typeFilter} setTypeFilter={setTypeFilter}
groupShown={groupShown} toggleGroup={toggleGroup}
scHidden={scHidden} toggleSc={toggleSc}
search={search} setSearch={setSearch}
counts={counts} selectedSc={selectedSc} onPickSc={onPickSc}
/>
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
{/* ТУЛБАР */}
<div style={{ height: 50, flex: "none", display: "flex", alignItems: "center", gap: 14, padding: "0 16px", borderBottom: "1px solid var(--line)", background: "var(--bg-1)" }}>
{/* масштаб */}
<div style={{ display: "flex", gap: 2, background: "var(--bg-0)", borderRadius: 7, padding: 3, border: "1px solid var(--line)" }}>
{SCALES.map((s) => (
<button key={s.id} onClick={() => { setScaleId(s.id); setScrollSignal((x) => x + 1); }}
style={{ padding: "5px 11px", borderRadius: 5, border: "none", fontSize: 11.5, fontWeight: 600, background: scaleId === s.id ? "var(--bg-3)" : "transparent", color: scaleId === s.id ? "var(--ink)" : "var(--ink-3)" }}>{s.label}</button>
))}
</div>
<button onClick={() => setScrollSignal((x) => x + 1)} style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 11px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg-2)", fontSize: 11.5, fontWeight: 600, color: "var(--now)" }}>
<span style={{ width: 6, height: 6, borderRadius: 9, background: "var(--now)", boxShadow: "0 0 6px var(--now)" }} />Сейчас
</button>
<div style={{ width: 1, height: 22, background: "var(--line)" }} />
<WorksLegend />
<div style={{ flex: 1 }} />
{/* статистика */}
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<Stat label="АППАРАТОВ" v={stats.sc} />
<Stat label="ПЛАНОВ" v={stats.iv} />
<Stat label="ПЕРЕСЕЧЕНИЙ" v={stats.ov} accent={stats.ov > 0 ? "var(--now)" : null} />
</div>
{!allOn && (
<button onClick={showAll} style={{ padding: "6px 11px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg-2)", fontSize: 11.5, color: "var(--ink-1)" }}>Показать все</button>
)}
</div>
{rows.length === 0 ? (
<div style={{ flex: 1, display: "grid", placeItems: "center", color: "var(--ink-3)", fontSize: 13 }}>
Нет аппаратов по текущим фильтрам.
</div>
) : (
<div style={{ flex: 1, minHeight: 0 }} onClick={() => {}}>
<Timeline
rows={rows} pxPerMs={pxPerMs} rangeStart={rangeStart} rangeEnd={rangeEnd}
now={D.NOW} selectedIv={selectedIv} onSelectIv={setSelectedIv}
lineageSet={lineageSet} byId={byId} scrollSignal={scrollSignal}
timelineCollapsed={timelineCollapsed} toggleCollapse={toggleCollapse}
/>
</div>
)}
</div>
<Details iv={selIv} sc={selSc} group={selGroup} byId={byId} onSelectIv={setSelectedIv} onClose={() => setSelectedIv(null)} />
</div>
</div>
);
}
function WorksLegend() {
const items = [
{ k: "shoot", label: "Съёмка", stripes: true },
{ k: "downlink", label: "Сброс", fill: "var(--accent)" },
{ k: "calib", label: "Калибровка", fill: "oklch(0.78 0.02 200)" },
{ k: "service", label: "Служебная", dashed: true },
];
return (
<div style={{ display: "flex", alignItems: "center", gap: 11 }}>
<span className="mono" style={{ fontSize: 8.5, color: "var(--ink-3)", letterSpacing: ".08em" }}>РАБОТЫ:</span>
{items.map((it) => (
<div key={it.k} style={{ display: "flex", alignItems: "center", gap: 5 }}>
<span style={{
width: 11, height: 9, borderRadius: 2, flex: "none",
background: it.stripes
? "linear-gradient(90deg, var(--t-optical) 0 33%, var(--t-radar) 33% 66%, var(--t-combo) 66% 100%)"
: it.dashed ? "transparent" : it.fill,
border: it.dashed ? "1px dashed var(--ink-3)" : "none",
}} />
<span style={{ fontSize: 10.5, color: "var(--ink-2)" }}>{it.label}</span>
</div>
))}
</div>
);
}
function Stat({ label, v, accent }) { return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", lineHeight: 1.1 }}>
<span className="mono tnum" style={{ fontSize: 15, fontWeight: 600, color: accent || "var(--ink)" }}>{v}</span>
<span className="mono" style={{ fontSize: 8, color: "var(--ink-3)", letterSpacing: ".08em" }}>{label}</span>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
@@ -0,0 +1,107 @@
// Web Mercator проекция + слой растровых тайлов (slippy-map) + кэш загрузки.
// Подложки: тёмная (CARTO, OSM-данные), классическая OSM, без тайлов (схема).
(function () {
const TILE = 256;
const DEG = Math.PI / 180, RAD = 180 / Math.PI;
const MAX_LAT = 85.0511;
// lon/lat -> мировые пиксели при размере мира ws (= TILE * 2^z).
function lonToWorldX(lon, ws) { return (lon + 180) / 360 * ws; }
function latToWorldY(lat, ws) {
const l = Math.max(-MAX_LAT, Math.min(MAX_LAT, lat));
const s = Math.sin(l * DEG);
const y = 0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI);
return y * ws;
}
function worldXToLon(wx, ws) { return wx / ws * 360 - 180; }
function worldYToLat(wy, ws) {
const n = Math.PI - 2 * Math.PI * (wy / ws);
return RAD * Math.atan(Math.sinh(n));
}
// Метры на пиксель в точке широты (для перевода км -> px).
function metersPerPixel(lat, ws) {
return Math.cos(lat * DEG) * 2 * Math.PI * 6378137 / ws;
}
const PROVIDERS = {
dark: {
id: "dark", name: "Тёмная",
url: (z, x, y) => `https://a.basemaps.cartocdn.com/dark_all/${z}/${x}/${y}.png`,
scrim: 0.0, label: "© OpenStreetMap · © CARTO", maxZoom: 18, dark: true,
},
osm: {
id: "osm", name: "Светлая",
url: (z, x, y) => `https://a.basemaps.cartocdn.com/rastertiles/voyager/${z}/${x}/${y}.png`,
scrim: 0.34, label: "© OpenStreetMap · © CARTO", maxZoom: 19, dark: false,
},
schema: {
id: "schema", name: "Схема", url: null, scrim: 0, label: "Схематичная основа (оффлайн)", dark: true,
},
};
// Кэш тайлов: key -> {img, loaded, error}.
const cache = new Map();
function getTile(provider, z, x, y, onLoad) {
const key = provider.id + "/" + z + "/" + x + "/" + y;
let rec = cache.get(key);
if (rec) return rec;
rec = { img: new Image(), loaded: false, error: false };
rec.img.crossOrigin = "anonymous";
rec.img.onload = () => { rec.loaded = true; onLoad && onLoad(); };
rec.img.onerror = () => { rec.error = true; };
rec.img.src = provider.url(z, x, y);
cache.set(key, rec);
return rec;
}
// Рисует слой тайлов на ctx для текущего вида.
// view: { centerLon, centerLat, z }, size: {w,h}. onLoad — колбэк перерисовки.
function drawTiles(ctx, provider, view, size, onLoad) {
if (!provider || !provider.url) return;
const { w, h } = size;
const z = view.z;
const tz = Math.max(1, Math.min(provider.maxZoom || 18, Math.round(z)));
const ws = TILE * Math.pow(2, z); // непрерывный размер мира
const k = Math.pow(2, z - tz); // масштаб тайла tz -> экран
const tileScreen = TILE * k;
const n = Math.pow(2, tz);
const cwx = lonToWorldX(view.centerLon, ws);
const cwy = latToWorldY(view.centerLat, ws);
const originX = cwx - w / 2; // мировой px (непрерывный) в левом верхнем углу
const originY = cwy - h / 2;
// Видимый диапазон тайлов в tz-пространстве.
const leftTz = (originX / k) / TILE;
const topTz = (originY / k) / TILE;
const rightTz = ((originX + w) / k) / TILE;
const botTz = ((originY + h) / k) / TILE;
const x0 = Math.floor(leftTz), x1 = Math.floor(rightTz);
const y0 = Math.max(0, Math.floor(topTz)), y1 = Math.min(n - 1, Math.floor(botTz));
ctx.imageSmoothingEnabled = true;
for (let tx = x0; tx <= x1; tx++) {
const wrapX = ((tx % n) + n) % n; // горизонтальный повтор мира
for (let ty = y0; ty <= y1; ty++) {
const rec = getTile(provider, tz, wrapX, ty, onLoad);
const sx = tx * tileScreen - originX;
const sy = ty * tileScreen - originY;
if (rec.loaded) {
ctx.drawImage(rec.img, Math.floor(sx), Math.floor(sy), Math.ceil(tileScreen) + 1, Math.ceil(tileScreen) + 1);
}
}
}
// затемняющий scrim (для светлой OSM, чтобы overlay читался)
if (provider.scrim > 0) {
ctx.fillStyle = `rgba(8,16,12,${provider.scrim})`;
ctx.fillRect(0, 0, w, h);
}
}
window.Basemap = {
TILE, MAX_LAT, PROVIDERS,
lonToWorldX, latToWorldY, worldXToLon, worldYToLat,
metersPerPixel, drawTiles, getTile,
};
})();
+195
View File
@@ -0,0 +1,195 @@
// Тестовые данные планов КА. Детерминированная генерация (seeded RNG),
// чтобы при перезагрузке данные были стабильны.
(function () {
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const rnd = mulberry32(20260529);
const pick = (arr) => arr[Math.floor(rnd() * arr.length)];
const randint = (a, b) => a + Math.floor(rnd() * (b - a + 1));
const HOUR = 3600 * 1000;
const DAY = 24 * HOUR;
// "Сейчас" фиксируем под текущую дату проекта.
const NOW = new Date("2026-05-29T11:20:00Z").getTime();
// Три типа по аппаратуре (все аппараты ДЗЗ).
const TYPES = {
optical: { id: "optical", label: "Оптическая", short: "ОПТ" },
radar: { id: "radar", label: "Радиолокационная", short: "РЛ" },
combo: { id: "combo", label: "Комбинированная", short: "ОПТ+РЛ" },
};
// Программы/созвездия (название группы). Каждая группа тяготеет к своим типам.
const GROUP_DEFS = [
{ id: "resurs", name: "Ресурс-ОП", code: "RSO", weights: { optical: 0.8, radar: 0.05, combo: 0.15 } },
{ id: "kanopus", name: "Канопус-В", code: "KNP", weights: { optical: 0.7, radar: 0.1, combo: 0.2 } },
{ id: "kondor", name: "Кондор-ФКА", code: "KND", weights: { optical: 0.05, radar: 0.85, combo: 0.1 } },
{ id: "obzor", name: "Обзор-Р", code: "OBZ", weights: { optical: 0.05, radar: 0.8, combo: 0.15 } },
{ id: "meteor", name: "Метеор-М", code: "MTR", weights: { optical: 0.55, radar: 0.15, combo: 0.3 } },
{ id: "aist", name: "Аист-2", code: "AST", weights: { optical: 0.6, radar: 0.1, combo: 0.3 } },
{ id: "smotr", name: "Смотр-Р", code: "SMT", weights: { optical: 0.1, radar: 0.55, combo: 0.35 } },
];
function weightedType(weights) {
const r = rnd();
let acc = 0;
for (const k of Object.keys(weights)) {
acc += weights[k];
if (r <= acc) return k;
}
return "optical";
}
// Виды РАБОТ внутри плана. У каждой работы своя категория (kind).
const WORK_KINDS = {
shoot: { id: "shoot", label: "Съёмка" },
downlink: { id: "downlink", label: "Сброс на НКПОР" },
calib: { id: "calib", label: "Калибровка" },
service: { id: "service", label: "Служебная операция" },
};
const WORK_KIND_ORDER = ["shoot", "downlink", "calib", "service"];
// Подпись работы-съёмки зависит от аппаратуры КА.
const SHOOT_LABEL = {
optical: ["Панхром. съёмка", "Мультиспектр. съёмка", "Стереосъёмка", "Площадная съёмка"],
radar: ["РСА съёмка", "Интерферометрия", "Прожекторный режим", "Сканирующий режим"],
combo: ["Совмещ. съёмка", "РСА + панхром.", "Площадная съёмка", "Мониторинг ЧС"],
};
// Названия самих ПЛАНОВ (объемлющих интервалов).
const PLAN_NAMES = ["Суточная программа", "Целевая программа", "Спецзадание", "Программа мониторинга", "Сеансовый план", "Программа ДЗЗ"];
function genWorks(planStart, planEnd, scType, makeId) {
const works = [];
const span = planEnd - planStart;
// Плотность работ ~ 1 на 49 часов плана.
let cursor = planStart + randint(0, 2) * HOUR;
let guard = 0;
while (cursor < planEnd - 1 * HOUR && guard < 40) {
guard++;
const kind = pick(WORK_KIND_ORDER);
let dur;
if (kind === "shoot") dur = randint(20, 140) * 60 * 1000; // 20м2.3ч
else if (kind === "downlink") dur = randint(8, 35) * 60 * 1000; // сброс
else if (kind === "calib") dur = randint(30, 90) * 60 * 1000;
else dur = randint(10, 40) * 60 * 1000; // служебная
const wStart = cursor;
const wEnd = Math.min(planEnd, wStart + dur);
if (wEnd <= wStart) break;
const label = kind === "shoot" ? pick(SHOOT_LABEL[scType]) : WORK_KINDS[kind].label;
works.push({ id: makeId(), kind, label, start: wStart, end: wEnd });
// пауза между работами
cursor = wEnd + randint(20, 180) * 60 * 1000;
}
return works;
}
// Генерация КА.
const spacecraft = [];
let scCounter = 0;
for (const g of GROUP_DEFS) {
const count = randint(8, 16); // суммарно ~80
for (let i = 0; i < count; i++) {
const type = weightedType(g.weights);
scCounter++;
const num = i + 1;
spacecraft.push({
id: `${g.id}-${num}`,
name: `${g.name}${num}`,
short: `${g.code}-${String(num).padStart(2, "0")}`,
groupId: g.id,
type,
orbit: pick(["ССО 510 км", "ССО 575 км", "ССО 700 км", "ССО 480 км"]),
});
}
}
// Видимый горизонт планирования: ~ -16 сут .. +30 сут от "сейчас".
const RANGE_START = NOW - 16 * DAY;
const RANGE_END = NOW + 30 * DAY;
function statusFor(start, end) {
if (end < NOW) return "executed";
if (start <= NOW && NOW <= end) return "active";
return "planned";
}
// Генерация ПЛАНОВ (объемлющих интервалов) с родословной (ревизии плана = потомки).
// Каждый план содержит набор РАБОТ внутри своего интервала.
const plans = [];
let ivCounter = 0;
let workCounter = 0;
const makeWorkId = () => `W-${String(++workCounter).padStart(5, "0")}`;
for (const sc of spacecraft) {
// стартовая точка цепочки
let cursor = RANGE_START + randint(0, 3) * DAY + randint(0, 20) * HOUR;
let prevId = null;
const nChains = randint(2, 4);
for (let c = 0; c < nChains; c++) {
// базовый план в цепочке
const dur = randint(8, 60) * HOUR;
const start = cursor;
const end = start + dur;
ivCounter++;
const baseId = `PL-${String(ivCounter).padStart(4, "0")}`;
const planName = pick(PLAN_NAMES);
plans.push({
id: baseId,
scId: sc.id,
start, end,
title: planName,
rev: 1,
parentId: prevId,
status: statusFor(start, end),
author: pick(["ЦУП-1", "ЦУП-2", "ПЦ Москва", "ПЦ Дубна", "Авто-планировщик"]),
works: genWorks(start, end, sc.type, makeWorkId),
});
prevId = baseId;
// c вероятностью ревизия, перекрывающая базовый план (создаёт пересечение)
if (rnd() < 0.45) {
const overlapStart = start + Math.floor(dur * (0.35 + rnd() * 0.4));
const revDur = randint(10, 50) * HOUR;
const revEnd = overlapStart + revDur;
ivCounter++;
const revId = `PL-${String(ivCounter).padStart(4, "0")}`;
plans.push({
id: revId,
scId: sc.id,
start: overlapStart,
end: revEnd,
title: planName,
rev: 2,
parentId: baseId,
status: statusFor(overlapStart, revEnd),
author: pick(["ЦУП-1", "ЦУП-2", "ПЦ Москва", "Авто-планировщик"]),
works: genWorks(overlapStart, revEnd, sc.type, makeWorkId),
});
prevId = revId;
cursor = revEnd + randint(4, 36) * HOUR;
} else {
cursor = end + randint(6, 48) * HOUR;
}
if (cursor > RANGE_END) break;
}
}
// Индексы родословной: дети по родителю.
const childrenOf = {};
for (const iv of plans) {
if (iv.parentId) {
(childrenOf[iv.parentId] = childrenOf[iv.parentId] || []).push(iv.id);
}
}
window.MissionData = {
HOUR, DAY, NOW, RANGE_START, RANGE_END,
TYPES, GROUP_DEFS, WORK_KINDS, WORK_KIND_ORDER,
spacecraft, plans, childrenOf,
typeOrder: ["optical", "radar", "combo"],
};
})();
@@ -0,0 +1,159 @@
// Правая панель деталей выбранного интервала + лента родословной (предки/потомки).
function StatusBadge({ status }) {
const map = {
executed: { t: "ВЫПОЛНЕН", c: "var(--ink-2)", bg: "var(--bg-3)" },
active: { t: "АКТИВЕН", c: "var(--now)", bg: "oklch(0.86 0.16 25 / 0.16)" },
planned: { t: "ЗАПЛАНИРОВАН", c: "var(--accent)", bg: "oklch(0.82 0.13 200 / 0.14)" },
};
const s = map[status] || map.planned;
return <span className="mono" style={{ fontSize: 9.5, fontWeight: 600, letterSpacing: ".06em", color: s.c, background: s.bg, padding: "2px 7px", borderRadius: 4 }}>{s.t}</span>;
}
function Field({ label, children, mono }) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>{label}</span>
<span className={mono ? "mono tnum" : ""} style={{ fontSize: 12.5, color: "var(--ink)" }}>{children}</span>
</div>
);
}
function Details({ iv, sc, group, byId, onSelectIv, onClose }) {
if (!iv) {
return (
<div style={{ height: "100%", background: "var(--bg-1)", borderLeft: "1px solid var(--line)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 10, padding: 24, textAlign: "center" }}>
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" style={{ opacity: .3 }}><rect x="3" y="6" width="18" height="3.4" rx="1.5" stroke="var(--ink-2)" strokeWidth="1.6" /><rect x="3" y="13" width="11" height="3.4" rx="1.5" stroke="var(--ink-2)" strokeWidth="1.6" /></svg>
<div style={{ fontSize: 12, color: "var(--ink-3)", maxWidth: 180 }}>Выберите план на таймлайне, чтобы увидеть его работы и цепочку ревизий.</div>
</div>
);
}
const lin = lineageOf(iv.id, byId);
const dur = iv.end - iv.start;
const col = TYPE_COLOR[sc.type];
const chainRow = (id) => {
const it = byId[id];
const isSel = id === iv.id;
return (
<button key={id} onClick={() => onSelectIv(id)} style={{
display: "flex", alignItems: "center", gap: 8, width: "100%", textAlign: "left",
padding: "7px 9px", borderRadius: 6, border: `1px solid ${isSel ? "var(--accent)" : "var(--line-soft)"}`,
background: isSel ? "oklch(0.82 0.13 200 / 0.12)" : "var(--bg-2)",
}}>
<span className="mono tnum" style={{ fontSize: 9.5, color: "var(--ink-3)", flex: "none" }}>{it.id}</span>
<span style={{ flex: 1, fontSize: 11.5, color: isSel ? "var(--ink)" : "var(--ink-1)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{it.title}</span>
<span className="mono" style={{ fontSize: 8.5, color: "var(--ink-3)", border: "1px solid var(--line)", borderRadius: 3, padding: "0 4px", flex: "none" }}>r{it.rev}</span>
</button>
);
};
return (
<div style={{ height: "100%", background: "var(--bg-1)", borderLeft: "1px solid var(--line)", display: "flex", flexDirection: "column" }}>
<div style={{ padding: "14px 16px 12px", borderBottom: "1px solid var(--line-soft)" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
<span style={{ width: 10, height: 10, borderRadius: 3, background: col, boxShadow: `0 0 8px ${col}`, flex: "none" }} />
<span className="mono" style={{ fontSize: 10, color: "var(--ink-3)" }}>{iv.id}</span>
</div>
<button onClick={onClose} style={{ background: "none", border: "none", color: "var(--ink-3)", fontSize: 18, lineHeight: 1, padding: 0 }}>×</button>
</div>
<div style={{ fontSize: 17, fontWeight: 700, marginTop: 8, lineHeight: 1.2 }}>{iv.title}</div>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 9 }}>
<StatusBadge status={iv.status} />
<span className="mono" style={{ fontSize: 10.5, color: "var(--ink-2)" }}>{sc.short} · {group.name}</span>
</div>
</div>
<div style={{ flex: 1, overflowY: "auto", padding: "14px 16px 20px", display: "flex", flexDirection: "column", gap: 16 }}>
{/* Время */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="НАЧАЛО" mono>{fmtDateTime(iv.start)}</Field>
<Field label="КОНЕЦ" mono>{fmtDateTime(iv.end)}</Field>
<Field label="ДЛИТЕЛЬНОСТЬ" mono>{fmtDur(dur)}</Field>
<Field label="РАБОТ В ПЛАНЕ" mono>{iv.works.length}</Field>
</div>
<div style={{ height: 1, background: "var(--line-soft)" }} />
{/* Аппарат */}
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>АППАРАТ</span>
<div style={{ display: "flex", alignItems: "center", gap: 9 }}>
<TypeDot type={sc.type} size={9} />
<div>
<div style={{ fontSize: 13, fontWeight: 600 }}>{sc.name}</div>
<div style={{ fontSize: 10.5, color: "var(--ink-3)" }}>{D.TYPES[sc.type].label} · {sc.orbit}</div>
</div>
</div>
<Field label="ИСТОЧНИК ПЛАНА">{iv.author}</Field>
</div>
<div style={{ height: 1, background: "var(--line-soft)" }} />
{/* Работы внутри плана */}
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>РАБОТЫ В ПЛАНЕ</span>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)" }}>{iv.works.length}</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
{iv.works.length === 0 && <div style={{ fontSize: 11, color: "var(--ink-3)" }}>Работы не заданы.</div>}
{iv.works.map((wk) => {
const fill = workFill(wk.kind, sc.type);
const isService = wk.kind === "service";
return (
<div key={wk.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, background: "var(--bg-2)" }}>
<span style={{ width: 9, height: 9, borderRadius: 2, flex: "none", background: isService ? "transparent" : fill, border: isService ? `1px dashed ${fill}` : "none" }} />
<span style={{ flex: 1, fontSize: 11.5, color: "var(--ink-1)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{wk.label}</span>
<span className="mono tnum" style={{ fontSize: 10, color: "var(--ink-3)", flex: "none" }}>{fmtTime(wk.start)}{fmtTime(wk.end)}</span>
</div>
);
})}
</div>
</div>
<div style={{ height: 1, background: "var(--line-soft)" }} />
{/* Родословная */}
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>РОДОСЛОВНАЯ ПЛАНА</span>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)" }}>{lin.chain.length} ревизий</span>
</div>
{lin.ancestors.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<span style={{ fontSize: 9.5, color: "var(--ink-3)" }}> Предки</span>
{lin.ancestors.map(chainRow)}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<span style={{ fontSize: 9.5, color: "var(--accent)" }}> Текущий</span>
{chainRow(iv.id)}
</div>
{lin.descendants.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<span style={{ fontSize: 9.5, color: "var(--ink-3)" }}> Потомки</span>
{lin.descendants.map(chainRow)}
</div>
)}
{lin.ancestors.length === 0 && lin.descendants.length === 0 && (
<div style={{ fontSize: 11, color: "var(--ink-3)", padding: "4px 0" }}>Это единственная ревизия плана без предков и потомков.</div>
)}
</div>
</div>
<div style={{ padding: "12px 16px", borderTop: "1px solid var(--line-soft)" }}>
<a href={`Map.html?plan=${iv.id}`} style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 7, padding: "9px 0", borderRadius: 7, border: "1px solid var(--line)", background: "var(--bg-2)", color: "var(--ink-1)", fontSize: 12, fontWeight: 600, textDecoration: "none" }}>
Показать на карте
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" /><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" stroke="currentColor" strokeWidth="1.4" fill="none" /></svg>
</a>
</div>
</div>
);
}
Object.assign(window, { Details });
@@ -0,0 +1,162 @@
// Страница «Карта»: вкладки, левые контролы (охват/слои/выбор КА), детали, ?plan=.
const { useState: useStateMA, useMemo: useMemoMA, useEffect: useEffectMA } = React;
const HOUR_MS = 3600000, DAY_MS = 86400000;
const PLAN_PAGE = "План КА — Mission Ops.html";
function MapApp() {
const byId = useMemoMA(() => { const m = {}; D.plans.forEach(p => m[p.id] = p); return m; }, []);
const scById = useMemoMA(() => { const m = {}; D.spacecraft.forEach(s => m[s.id] = s); return m; }, []);
const ivBySc = useMemoMA(() => { const m = {}; D.plans.forEach(p => (m[p.scId] = m[p.scId] || []).push(p)); return m; }, []);
const paramsBy = useMemoMA(() => { const m = {}; D.spacecraft.forEach(s => m[s.id] = orbitParams(s)); return m; }, []);
// начальное состояние из URL
const urlParams = new URLSearchParams(location.search);
const urlPlan = urlParams.get("plan");
const urlSc = urlParams.get("sc");
const initPlan = urlPlan && byId[urlPlan] ? urlPlan : null;
const initSc = initPlan ? byId[initPlan].scId : (urlSc && scById[urlSc] ? urlSc : D.spacecraft[0].id);
const [scope, setScope] = useStateMA(initPlan || urlSc ? "one" : "one");
const [selSc, setSelSc] = useStateMA(initSc);
const [selPlan, setSelPlan] = useStateMA(initPlan);
const [search, setSearch] = useStateMA("");
const [layers, setLayers] = useStateMA({ tracks: true, swath: true, shoot: true, downlink: true, stations: true, terminator: true });
const [basemap, setBasemap] = useStateMA("dark");
const sc = scById[selSc];
const plan = selPlan ? byId[selPlan] : null;
// окно времени
const win = useMemoMA(() => {
if (plan) return { t0: plan.start, t1: Math.min(plan.end, plan.start + 3 * DAY_MS) };
return { t0: D.NOW - 3 * HOUR_MS, t1: D.NOW + 9 * HOUR_MS };
}, [plan]);
// список КА в охвате
const scList = useMemoMA(() => {
if (scope === "one") return [sc];
if (scope === "group") return D.spacecraft.filter(s => s.groupId === sc.groupId);
return D.spacecraft;
}, [scope, sc]);
const toggleLayer = (k) => setLayers(l => ({ ...l, [k]: !l[k] }));
const onSelectSc = (id) => { setSelSc(id); setSelPlan(null); if (scope === "all") setScope("group"); };
const onSelectPlan = (id) => { const p = byId[id]; setSelSc(p.scId); setSelPlan(id); setScope("one"); };
// фильтр выбора КА
const q = search.trim().toLowerCase();
const scResults = useMemoMA(() => D.spacecraft.filter(s => !q || s.name.toLowerCase().includes(q) || s.short.toLowerCase().includes(q)), [q]);
return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<TopTabs active="map" planHref={PLAN_PAGE} />
<div style={{ flex: 1, display: "grid", gridTemplateColumns: `288px 1fr ${plan ? "330px" : "0px"}`, minHeight: 0, transition: "grid-template-columns .18s" }}>
{/* ЛЕВЫЕ КОНТРОЛЫ */}
<div style={{ background: "var(--bg-1)", borderRight: "1px solid var(--line)", display: "flex", flexDirection: "column", minHeight: 0 }}>
{/* подложка */}
<div style={{ padding: "12px 14px", borderBottom: "1px solid var(--line-soft)" }}>
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 7 }}>ПОДЛОЖКА</div>
<div style={{ display: "flex", gap: 3, background: "var(--bg-0)", borderRadius: 7, padding: 3, border: "1px solid var(--line)" }}>
{[["dark", "Тёмная"], ["osm", "Светлая"], ["schema", "Схема"]].map(([id, lab]) => (
<button key={id} onClick={() => setBasemap(id)} style={{ flex: 1, padding: "6px 0", borderRadius: 5, border: "none", fontSize: 11.5, fontWeight: 600, background: basemap === id ? "var(--bg-3)" : "transparent", color: basemap === id ? "var(--ink)" : "var(--ink-3)" }}>{lab}</button>
))}
</div>
</div>
{/* охват */}
<div style={{ padding: "12px 14px", borderBottom: "1px solid var(--line-soft)" }}>
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 7 }}>ОХВАТ</div>
<div style={{ display: "flex", gap: 3, background: "var(--bg-0)", borderRadius: 7, padding: 3, border: "1px solid var(--line)" }}>
{[["one", "1 КА"], ["group", "Группа"], ["all", "Все"]].map(([id, lab]) => (
<button key={id} onClick={() => setScope(id)} style={{ flex: 1, padding: "6px 0", borderRadius: 5, border: "none", fontSize: 11.5, fontWeight: 600, background: scope === id ? "var(--bg-3)" : "transparent", color: scope === id ? "var(--ink)" : "var(--ink-3)" }}>{lab}</button>
))}
</div>
<div style={{ marginTop: 8, fontSize: 11, color: "var(--ink-2)", display: "flex", alignItems: "center", gap: 6 }}>
<TypeDot type={sc.type} size={8} />
<span className="mono">{sc.short}</span>
<span style={{ color: "var(--ink-3)" }}>· {D.GROUP_DEFS.find(g => g.id === sc.groupId).name}</span>
</div>
</div>
{/* окно времени */}
<div style={{ padding: "10px 14px", borderBottom: "1px solid var(--line-soft)" }}>
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 5 }}>ОКНО ВРЕМЕНИ</div>
<div className="mono" style={{ fontSize: 11, color: "var(--ink-1)" }}>{fmtDateTime(win.t0)} {fmtDateTime(win.t1)}</div>
<div style={{ fontSize: 10, color: "var(--ink-3)", marginTop: 3 }}>{plan ? `план ${plan.id}` : "сутки вокруг «сейчас»"}</div>
</div>
{/* слои */}
<div style={{ padding: "10px 14px", borderBottom: "1px solid var(--line-soft)" }}>
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 7 }}>СЛОИ</div>
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<LayerRow on={layers.tracks} onClick={() => toggleLayer("tracks")} swatch={<LineSwatch />} label="Трассы КА" />
<LayerRow on={layers.swath} onClick={() => toggleLayer("swath")} swatch={<BandSwatch />} label="Полосы обзора (swath)" />
<LayerRow on={layers.shoot} onClick={() => toggleLayer("shoot")} swatch={<PolySwatch />} label="Контуры маршрутов съёмки" />
<LayerRow on={layers.downlink} onClick={() => toggleLayer("downlink")} swatch={<DownSwatch />} label="Участки сброса" />
<LayerRow on={layers.stations} onClick={() => toggleLayer("stations")} swatch={<StationSwatch />} label="Станции приёма (НКПОР)" />
<LayerRow on={layers.terminator} onClick={() => toggleLayer("terminator")} swatch={<NightSwatch />} label="Терминатор день/ночь" />
</div>
</div>
{/* выбор КА */}
<div style={{ padding: "10px 14px 6px" }}>
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 6 }}>АППАРАТ В ФОКУСЕ</div>
<div style={{ position: "relative" }}>
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Поиск КА…"
style={{ width: "100%", padding: "7px 8px", background: "var(--bg-0)", border: "1px solid var(--line)", borderRadius: 6, color: "var(--ink)", fontFamily: "var(--mono)", fontSize: 12, outline: "none" }} />
</div>
</div>
<div style={{ flex: 1, overflowY: "auto", padding: "4px 8px 12px" }}>
{scResults.map((s) => (
<div key={s.id} onClick={() => onSelectSc(s.id)}
style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, cursor: "pointer", background: s.id === selSc ? "var(--bg-3)" : "transparent" }}>
<TypeDot type={s.type} size={7} />
<span className="mono" style={{ flex: 1, fontSize: 11, color: s.id === selSc ? "var(--ink)" : "var(--ink-1)" }}>{s.short}</span>
<span className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)" }}>{(ivBySc[s.id] || []).length} пл.</span>
</div>
))}
</div>
</div>
{/* КАРТА */}
<div style={{ position: "relative", minWidth: 0 }}>
<MapView scList={scList} selectedPlan={plan} window={win} now={D.NOW} layers={layers}
onSelectSc={onSelectSc} paramsBy={paramsBy} scById={scById} basemap={basemap} />
{/* плашка масштаба/охвата */}
<div style={{ position: "absolute", left: 12, top: 12, display: "flex", gap: 8, alignItems: "center", background: "var(--bg-1)", border: "1px solid var(--line)", borderRadius: 7, padding: "6px 10px" }}>
<span className="mono" style={{ fontSize: 10, color: "var(--ink-3)" }}>ОХВАТ:</span>
<span style={{ fontSize: 11.5, fontWeight: 600 }}>{scope === "one" ? "1 КА" : scope === "group" ? "Группа" : "Все"}</span>
<span style={{ width: 1, height: 14, background: "var(--line)" }} />
<span className="mono tnum" style={{ fontSize: 11.5, color: "var(--ink-1)" }}>{scList.length} КА</span>
</div>
</div>
{/* ДЕТАЛИ ПЛАНА */}
{plan && <MapDetails plan={plan} sc={scById[plan.scId]} params={paramsBy[plan.scId]} byId={byId} onSelectPlan={onSelectPlan} onClose={() => setSelPlan(null)} planPage={PLAN_PAGE} />}
</div>
</div>
);
}
function LayerRow({ on, onClick, swatch, label }) {
return (
<button onClick={onClick} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 6px", borderRadius: 6, border: "1px solid transparent", background: on ? "var(--bg-2)" : "transparent", opacity: on ? 1 : 0.4, textAlign: "left" }}>
<span style={{ width: 18, flex: "none", display: "grid", placeItems: "center" }}>{swatch}</span>
<span style={{ flex: 1, fontSize: 11.5, color: "var(--ink-1)" }}>{label}</span>
<span style={{ width: 14, height: 14, borderRadius: 4, flex: "none", border: `1.5px solid ${on ? "var(--accent)" : "var(--line)"}`, background: on ? "var(--accent)" : "transparent", display: "grid", placeItems: "center" }}>
{on && <svg width="9" height="9" viewBox="0 0 10 10"><path d="M1.5 5.2 4 7.6 8.6 2.4" stroke="var(--bg-0)" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>}
</span>
</button>
);
}
const LineSwatch = () => <svg width="18" height="10"><path d="M0 8 Q6 0 12 5 T18 3" stroke="var(--t-optical)" strokeWidth="1.4" fill="none" /></svg>;
const BandSwatch = () => <svg width="18" height="10"><path d="M0 8 Q6 0 12 5 T18 3" stroke="var(--t-optical)" strokeWidth="5" fill="none" opacity="0.25" /></svg>;
const PolySwatch = () => <svg width="18" height="10"><rect x="2" y="2" width="14" height="6" fill="color-mix(in oklch, var(--t-optical) 30%, transparent)" stroke="var(--t-optical)" strokeWidth="1" /></svg>;
const DownSwatch = () => <svg width="18" height="10"><path d="M1 5 H17" stroke="var(--accent)" strokeWidth="3" strokeLinecap="round" /></svg>;
const StationSwatch = () => <svg width="18" height="10"><path d="M9 1 L13 8 L5 8 Z" fill="rgba(180,200,210,0.9)" /></svg>;
const NightSwatch = () => <svg width="18" height="10"><rect x="0" y="0" width="9" height="10" fill="rgba(4,8,12,0.6)" /><rect x="9" y="0" width="9" height="10" fill="#15291f" /></svg>;
ReactDOM.createRoot(document.getElementById("root")).render(<MapApp />);
@@ -0,0 +1,125 @@
// Панель деталей плана на странице «Карта»: сводка, наземные параметры,
// работы (со «съёмка/сброс»), ссылка обратно на таймлайн.
function MapStatusBadge({ status }) {
const map = {
executed: { t: "ВЫПОЛНЕН", c: "var(--ink-2)", bg: "var(--bg-3)" },
active: { t: "АКТИВЕН", c: "var(--now)", bg: "oklch(0.86 0.16 25 / 0.16)" },
planned: { t: "ЗАПЛАНИРОВАН", c: "var(--accent)", bg: "oklch(0.82 0.13 200 / 0.14)" },
};
const s = map[status] || map.planned;
return <span className="mono" style={{ fontSize: 9.5, fontWeight: 600, letterSpacing: ".06em", color: s.c, background: s.bg, padding: "2px 7px", borderRadius: 4 }}>{s.t}</span>;
}
function MapDetails({ plan, sc, params, byId, onSelectPlan, onClose, planPage }) {
const col = TYPE_COLOR[sc.type];
const shoots = plan.works.filter(w => w.kind === "shoot");
const downs = plan.works.filter(w => w.kind === "downlink");
const STATIONS = window.WorldGeo.GROUND_STATIONS;
// станции, задействованные в сбросах
const usedStations = [];
for (const d of downs) {
const pts = trackPoints(params, d.start, d.end, 10);
const mid = pts[Math.floor(pts.length / 2)];
const ns = nearestStation(mid.lat, mid.lon, STATIONS);
if (ns.st && !usedStations.find(x => x.id === ns.st.id)) usedStations.push(ns.st);
}
return (
<div style={{ background: "var(--bg-1)", borderLeft: "1px solid var(--line)", display: "flex", flexDirection: "column", minHeight: 0 }}>
<div style={{ padding: "14px 16px 12px", borderBottom: "1px solid var(--line-soft)" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
<span style={{ width: 10, height: 10, borderRadius: 3, background: col, boxShadow: `0 0 8px ${col}`, flex: "none" }} />
<span className="mono" style={{ fontSize: 10, color: "var(--ink-3)" }}>{plan.id}</span>
</div>
<button onClick={onClose} style={{ background: "none", border: "none", color: "var(--ink-3)", fontSize: 18, lineHeight: 1, padding: 0 }}>×</button>
</div>
<div style={{ fontSize: 16, fontWeight: 700, marginTop: 8, lineHeight: 1.2 }}>{plan.title}</div>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 9 }}>
<MapStatusBadge status={plan.status} />
<span className="mono" style={{ fontSize: 10.5, color: "var(--ink-2)" }}>{sc.short} · {sc.orbit}</span>
</div>
</div>
<div style={{ flex: 1, overflowY: "auto", padding: "14px 16px 20px", display: "flex", flexDirection: "column", gap: 15 }}>
{/* наземные параметры */}
<div>
<div className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em", marginBottom: 8 }}>НАЗЕМНЫЕ ПАРАМЕТРЫ</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<KV label="ВЫСОТА ОРБИТЫ">{params.h} км</KV>
<KV label="ПЕРИОД ВИТКА">{(params.T / 60).toFixed(1)} мин</KV>
<KV label="ПОЛОСА ОБЗОРА">{Math.round(params.swathKm)} км</KV>
<KV label="НАКЛОНЕНИЕ">{(params.incl * RAD).toFixed(1)}°</KV>
</div>
</div>
<div style={{ height: 1, background: "var(--line-soft)" }} />
{/* съёмка */}
<div>
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 8 }}>
<span style={{ width: 10, height: 8, background: col, borderRadius: 2, flex: "none" }} />
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>КОНТУРЫ СЪЁМКИ</span>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", marginLeft: "auto" }}>{shoots.length}</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
{shoots.length === 0 && <div style={{ fontSize: 11, color: "var(--ink-3)" }}>Съёмочных маршрутов нет.</div>}
{shoots.map(w => (
<div key={w.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, background: "var(--bg-2)" }}>
<span style={{ flex: 1, fontSize: 11.5, color: "var(--ink-1)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{w.label}</span>
<span className="mono tnum" style={{ fontSize: 10, color: "var(--ink-3)" }}>{fmtTime(w.start)}{fmtTime(w.end)}</span>
</div>
))}
</div>
</div>
{/* сброс */}
<div>
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 8 }}>
<span style={{ width: 12, height: 3, background: "var(--accent)", borderRadius: 2, flex: "none", boxShadow: "0 0 5px var(--accent)" }} />
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>УЧАСТКИ СБРОСА</span>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", marginLeft: "auto" }}>{downs.length}</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
{downs.length === 0 && <div style={{ fontSize: 11, color: "var(--ink-3)" }}>Сбросов нет.</div>}
{downs.map(w => {
const pts = trackPoints(params, w.start, w.end, 10);
const mid = pts[Math.floor(pts.length / 2)];
const ns = nearestStation(mid.lat, mid.lon, STATIONS);
return (
<div key={w.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, background: "var(--bg-2)" }}>
<span style={{ flex: 1, fontSize: 11.5, color: "var(--ink-1)" }}> {ns.st.name}</span>
<span className="mono tnum" style={{ fontSize: 10, color: "var(--ink-3)" }}>{fmtTime(w.start)}{fmtTime(w.end)}</span>
</div>
);
})}
</div>
</div>
{usedStations.length > 0 && (
<div style={{ fontSize: 10.5, color: "var(--ink-3)", lineHeight: 1.5 }}>
Задействованы станции: <span style={{ color: "var(--ink-1)" }}>{usedStations.map(s => s.name).join(", ")}</span>
</div>
)}
</div>
<div style={{ padding: "12px 16px", borderTop: "1px solid var(--line-soft)" }}>
<a href={`${planPage}?plan=${plan.id}`} style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 7, padding: "9px 0", borderRadius: 7, border: "1px solid var(--line)", background: "var(--bg-2)", color: "var(--ink-1)", fontSize: 12, fontWeight: 600, textDecoration: "none" }}>
Открыть в таймлайне планов
</a>
</div>
</div>
);
}
function KV({ label, children }) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".06em" }}>{label}</span>
<span className="mono tnum" style={{ fontSize: 12.5, color: "var(--ink)" }}>{children}</span>
</div>
);
}
Object.assign(window, { MapDetails });
@@ -0,0 +1,300 @@
// Карта: Web Mercator. Canvas-основа (тайлы OSM/CARTO или векторная схема,
// сетка, трассы, swath, терминатор) + SVG-overlay (станции, «сейчас»,
// контуры съёмки, сброс, hover). Зум/панорама как у slippy-карты.
const { useRef: useRefMV, useState: useStateMV, useEffect: useEffectMV, useMemo: useMemoMV, useCallback } = React;
const STATIONS = window.WorldGeo.GROUND_STATIONS;
const BM = window.Basemap;
const TILE = BM.TILE;
function MapView(props) {
const { scList, selectedPlan, window: win, now, layers, onSelectSc, paramsBy, scById, basemap } = props;
const wrapRef = useRefMV(null);
const canvasRef = useRefMV(null);
const [dims, setDims] = useStateMV({ w: 1000, h: 560 });
const [view, setView] = useStateMV({ centerLon: 45, centerLat: 30, z: 2.2, init: false });
const [hover, setHover] = useStateMV(null);
const [tileTick, setTileTick] = useStateMV(0);
const drag = useRefMV(null);
const provider = BM.PROVIDERS[basemap] || BM.PROVIDERS.dark;
const ws = useMemoMV(() => TILE * Math.pow(2, view.z), [view.z]);
// Инициализация: вписать мир по ширине.
useEffectMV(() => {
if (view.init || !dims.w) return;
const z = Math.max(0.7, Math.log2(dims.w / TILE) - 0.05);
setView((v) => ({ ...v, z, centerLon: 45, centerLat: 28, init: true }));
}, [dims, view.init]);
const project = useCallback((lon, lat) => {
const cwx = BM.lonToWorldX(view.centerLon, ws);
const cwy = BM.latToWorldY(view.centerLat, ws);
let wx = BM.lonToWorldX(lon, ws);
// выбрать ближайшую копию мира по X (горизонтальный повтор)
const half = ws / 2;
while (wx - cwx > half) wx -= ws;
while (wx - cwx < -half) wx += ws;
return {
x: dims.w / 2 + (wx - cwx),
y: dims.h / 2 + (BM.latToWorldY(lat, ws) - cwy),
};
}, [view.centerLon, view.centerLat, ws, dims]);
const unproject = useCallback((sx, sy) => {
const cwx = BM.lonToWorldX(view.centerLon, ws);
const cwy = BM.latToWorldY(view.centerLat, ws);
return {
lon: BM.worldXToLon(cwx + (sx - dims.w / 2), ws),
lat: BM.worldYToLat(cwy + (sy - dims.h / 2), ws),
};
}, [view.centerLon, view.centerLat, ws, dims]);
// ResizeObserver
useEffectMV(() => {
const el = wrapRef.current;
if (!el) return;
const ro = new ResizeObserver(() => setDims({ w: el.clientWidth, h: el.clientHeight }));
ro.observe(el);
setDims({ w: el.clientWidth, h: el.clientHeight });
return () => ro.disconnect();
}, []);
// Отрисовка canvas.
useEffectMV(() => {
const cv = canvasRef.current;
if (!cv) return;
const dpr = Math.min(2, window.devicePixelRatio || 1);
cv.width = dims.w * dpr; cv.height = dims.h * dpr;
cv.style.width = dims.w + "px"; cv.style.height = dims.h + "px";
const ctx = cv.getContext("2d");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, dims.w, dims.h);
// фон
ctx.fillStyle = provider.dark ? "#0a1410" : "#0a1410";
ctx.fillRect(0, 0, dims.w, dims.h);
const P = (lon, lat) => project(lon, lat);
if (provider.url) {
// растровая подложка (тайлы)
BM.drawTiles(ctx, provider, view, dims, () => setTileTick((t) => t + 1));
} else {
// векторная схема (оффлайн)
drawVectorWorld(ctx, P, dims);
}
// сетка координат поверх (тонкая)
ctx.strokeStyle = provider.dark ? "rgba(120,180,150,0.10)" : "rgba(255,255,255,0.14)";
ctx.lineWidth = 1;
ctx.beginPath();
for (let lon = -180; lon <= 180; lon += 30) { const a = P(lon, 84), b = P(lon, -84); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); }
for (let lat = -60; lat <= 60; lat += 30) { const a = P(-180, lat), b = P(180, lat); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); }
ctx.stroke();
// терминатор / ночь
if (layers.terminator) {
const tc = terminatorCurve(now);
ctx.beginPath();
tc.curve.forEach((pt, i) => { const q = P(pt[0], pt[1]); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
const topLat = tc.nightInNorth ? 84 : -84;
const c1 = P(180, topLat), c2 = P(-180, topLat);
ctx.lineTo(c1.x, c1.y); ctx.lineTo(c2.x, c2.y);
ctx.closePath();
ctx.fillStyle = "rgba(4,8,12,0.42)";
ctx.fill();
ctx.strokeStyle = "rgba(255,210,120,0.30)";
ctx.lineWidth = 1;
ctx.beginPath();
tc.curve.forEach((pt, i) => { const q = P(pt[0], pt[1]); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
ctx.stroke();
}
// трассы + swath
const TYPE_RGB = { optical: [126,222,180], radar: [232,200,120], combo: [240,150,165] };
const many = scList.length;
const showSwath = layers.swath && many <= 8;
const trackAlpha = many > 40 ? 0.14 : many > 14 ? 0.22 : many > 4 ? 0.4 : 0.72;
for (const sc of scList) {
const p = paramsBy[sc.id];
const rgb = TYPE_RGB[sc.type];
const segs = groundTrack(p, win.t0, win.t1, 900);
const isSelSc = selectedPlan && selectedPlan.scId === sc.id;
if (showSwath || isSelSc) {
for (const seg of segs) {
if (!seg.length) continue;
const midLat = seg[Math.floor(seg.length / 2)].lat;
const mpp = BM.metersPerPixel(midLat, ws);
const wpx = Math.max(1.5, (p.swathKm * 1000) / mpp);
ctx.strokeStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${isSelSc ? 0.16 : 0.09})`;
ctx.lineWidth = wpx; ctx.lineCap = "round"; ctx.lineJoin = "round";
ctx.beginPath();
seg.forEach((pt, i) => { const q = P(pt.lon, pt.lat); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
ctx.stroke();
}
}
if (layers.tracks) {
ctx.strokeStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${isSelSc ? 0.98 : trackAlpha})`;
ctx.lineWidth = isSelSc ? 2 : many > 14 ? 0.6 : 0.9;
for (const seg of segs) {
ctx.beginPath();
seg.forEach((pt, i) => { const q = P(pt.lon, pt.lat); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
ctx.stroke();
}
}
}
}, [dims, view, scList, selectedPlan, win, now, layers, project, ws, provider, tileTick]);
// ---- overlay (SVG) ----
const planOverlay = useMemoMV(() => {
if (!selectedPlan) return null;
const p = paramsBy[selectedPlan.scId];
const sc = scById[selectedPlan.scId];
const shoots = [], downs = [];
for (const wk of selectedPlan.works) {
if (wk.kind === "shoot" && layers.shoot) {
const pts = trackPoints(p, wk.start, wk.end, 18);
const poly = ribbon(pts, p.swathKm / 2);
shoots.push({ id: wk.id, label: wk.label, poly: poly.map(c => project(c[0], c[1])) });
}
if (wk.kind === "downlink" && layers.downlink) {
const pts = trackPoints(p, wk.start, wk.end, 14);
const mid = pts[Math.floor(pts.length / 2)];
const ns = nearestStation(mid.lat, mid.lon, STATIONS);
downs.push({
id: wk.id, label: wk.label,
line: pts.map(pt => project(pt.lon, pt.lat)),
st: ns.st, stPt: project(ns.st.lon, ns.st.lat), midPt: project(mid.lon, mid.lat),
});
}
}
return { shoots, downs, scType: sc.type };
}, [selectedPlan, layers, project, paramsBy, scById]);
const nowMarkers = useMemoMV(() => {
if (now < win.t0 || now > win.t1) return [];
return scList.map((sc) => {
const s = subPoint(paramsBy[sc.id], now);
return { id: sc.id, short: sc.short, type: sc.type, ...project(s.lon, s.lat) };
});
}, [scList, now, win, project, paramsBy]);
const TYPE_HEX = { optical: "var(--t-optical)", radar: "var(--t-radar)", combo: "var(--t-combo)" };
// ---- pan / zoom ----
const onWheel = (e) => {
e.preventDefault();
const rect = wrapRef.current.getBoundingClientRect();
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
const before = unproject(mx, my);
setView((v) => {
const nz = Math.max(0.7, Math.min(9, v.z + (e.deltaY < 0 ? 0.4 : -0.4)));
const nws = TILE * Math.pow(2, nz);
const cwx = BM.lonToWorldX(before.lon, nws) - (mx - dims.w / 2);
const cwy = BM.latToWorldY(before.lat, nws) - (my - dims.h / 2);
return { ...v, z: nz, centerLon: BM.worldXToLon(cwx, nws), centerLat: BM.worldYToLat(cwy, nws) };
});
};
const onDown = (e) => {
drag.current = { x: e.clientX, y: e.clientY, lon: view.centerLon, lat: view.centerLat, moved: false };
};
const onMove = (e) => {
if (!drag.current) return;
const dx = e.clientX - drag.current.x, dy = e.clientY - drag.current.y;
if (Math.abs(dx) + Math.abs(dy) > 3) drag.current.moved = true;
const cwx = BM.lonToWorldX(drag.current.lon, ws) - dx;
const cwy = BM.latToWorldY(drag.current.lat, ws) - dy;
setView((v) => ({ ...v, centerLon: BM.worldXToLon(cwx, ws), centerLat: Math.max(-82, Math.min(82, BM.worldYToLat(cwy, ws))) }));
};
const onUp = () => { drag.current = null; };
const zoomBy = (f) => setView((v) => ({ ...v, z: Math.max(0.7, Math.min(9, v.z + f)) }));
const resetView = () => setView((v) => ({ ...v, init: false }));
return (
<div ref={wrapRef} style={{ position: "absolute", inset: 0, overflow: "hidden", cursor: drag.current ? "grabbing" : "grab", background: "#0a1410" }}
onWheel={onWheel} onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp}>
<canvas ref={canvasRef} style={{ position: "absolute", inset: 0 }} />
<svg style={{ position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none", overflow: "visible" }}>
{planOverlay && planOverlay.shoots.map((s) => (
<polygon key={s.id} points={s.poly.map(p => `${p.x},${p.y}`).join(" ")}
fill={`color-mix(in oklch, ${TYPE_HEX[planOverlay.scType]} 32%, transparent)`}
stroke={TYPE_HEX[planOverlay.scType]} strokeWidth="1.4" style={{ pointerEvents: "all", cursor: "pointer" }}
onMouseEnter={(e) => setHover({ x: e.clientX, y: e.clientY, title: s.label, kind: "Съёмка / контур маршрута" })}
onMouseLeave={() => setHover(null)} />
))}
{planOverlay && planOverlay.downs.map((d) => (
<g key={d.id}>
<line x1={d.midPt.x} y1={d.midPt.y} x2={d.stPt.x} y2={d.stPt.y} stroke="var(--accent)" strokeWidth="1" strokeDasharray="2 3" opacity="0.6" />
<polyline points={d.line.map(p => `${p.x},${p.y}`).join(" ")} fill="none"
stroke="var(--accent)" strokeWidth="3.4" strokeLinecap="round"
style={{ pointerEvents: "all", cursor: "pointer", filter: "drop-shadow(0 0 4px var(--accent))" }}
onMouseEnter={(e) => setHover({ x: e.clientX, y: e.clientY, title: `${d.label}${d.st.name}`, kind: "Сброс на НКПОР" })}
onMouseLeave={() => setHover(null)} />
</g>
))}
{layers.stations && STATIONS.map((st) => {
const q = project(st.lon, st.lat);
const active = planOverlay && planOverlay.downs.some(d => d.st.id === st.id);
return (
<g key={st.id} transform={`translate(${q.x},${q.y})`} style={{ pointerEvents: "all", cursor: "default" }}
onMouseEnter={(e) => setHover({ x: e.clientX, y: e.clientY, title: st.name, kind: "Станция приёма (НКПОР)" })}
onMouseLeave={() => setHover(null)}>
<path d="M0,-6 L5,4 L-5,4 Z" fill={active ? "var(--accent)" : "rgba(190,210,220,0.92)"} stroke="#0a1410" strokeWidth="1" />
{view.z > 3.6 && <text x="7" y="3" fontSize="9" fill={provider.dark ? "var(--ink-2)" : "#dfeaf0"} style={{ fontFamily: "var(--mono)", paintOrder: "stroke", stroke: "#0a1410", strokeWidth: 2 }}>{st.name}</text>}
</g>
);
})}
{nowMarkers.map((m) => (
<g key={m.id} transform={`translate(${m.x},${m.y})`} style={{ pointerEvents: "all", cursor: "pointer" }}
onClick={() => onSelectSc(m.id)}
onMouseEnter={(e) => setHover({ x: e.clientX, y: e.clientY, title: m.short, kind: "Подспутниковая точка · сейчас" })}
onMouseLeave={() => setHover(null)}>
<circle r="4.5" fill={TYPE_HEX[m.type]} stroke="#0a1410" strokeWidth="1.5" />
<circle r="8" fill="none" stroke={TYPE_HEX[m.type]} strokeWidth="1" opacity="0.5" />
</g>
))}
</svg>
{hover && (
<div style={{ position: "fixed", left: hover.x + 12, top: hover.y + 12, zIndex: 50, pointerEvents: "none",
background: "var(--bg-0)", border: "1px solid var(--line)", borderRadius: 6, padding: "6px 9px", boxShadow: "var(--shadow)", maxWidth: 220 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: "var(--ink)" }}>{hover.title}</div>
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", marginTop: 2 }}>{hover.kind}</div>
</div>
)}
{/* атрибуция подложки */}
<div className="mono" style={{ position: "absolute", left: 8, bottom: 6, fontSize: 9, color: provider.dark ? "var(--ink-3)" : "rgba(255,255,255,0.8)", background: "rgba(10,20,16,0.55)", padding: "2px 6px", borderRadius: 4, pointerEvents: "none" }}>{provider.label}</div>
<div style={{ position: "absolute", right: 12, bottom: 12, display: "flex", flexDirection: "column", gap: 4 }}>
<ZoomBtn onClick={() => zoomBy(0.5)}>+</ZoomBtn>
<ZoomBtn onClick={() => zoomBy(-0.5)}></ZoomBtn>
<ZoomBtn onClick={resetView} title="Сбросить вид"></ZoomBtn>
</div>
</div>
);
}
// Векторная схема континентов (режим «Схема», оффлайн).
function drawVectorWorld(ctx, P, dims) {
for (const name in window.WorldGeo.LAND) {
const poly = window.WorldGeo.LAND[name];
ctx.beginPath();
poly.forEach((pt, i) => { const q = P(pt[0], pt[1]); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
ctx.closePath();
ctx.fillStyle = "#173228"; ctx.fill();
ctx.strokeStyle = "rgba(150,215,180,0.5)"; ctx.lineWidth = 0.9; ctx.stroke();
}
}
function ZoomBtn({ children, onClick, title }) {
return (
<button onClick={onClick} title={title} style={{ width: 30, height: 30, borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg-1)", color: "var(--ink-1)", fontSize: 16, lineHeight: 1, display: "grid", placeItems: "center" }}>{children}</button>
);
}
Object.assign(window, { MapView });
@@ -0,0 +1,142 @@
// Орбитальная геометрия: трассы КА (подспутниковая точка), полосы обзора (swath),
// контуры маршрутов съёмки, участки сброса, терминатор день/ночь.
const DEG = Math.PI / 180;
const RAD = 180 / Math.PI;
const RE_KM = 6371;
const MU = 398600.4418; // км^3/с^2
const SIDEREAL = 86164; // период вращения Земли, с
// Детерминированный хэш строки [0,1).
function hash01(str, salt) {
let h = 2166136261 ^ (salt || 0);
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
return ((h >>> 0) % 100000) / 100000;
}
// Параметры орбиты КА (ССО). Высоту берём из sc.orbit, наклонение ~98°.
function orbitParams(sc) {
const m = /(\d+)\s*км/.exec(sc.orbit);
const h = m ? +m[1] : 550;
const a = RE_KM + h;
const T = 2 * Math.PI * Math.sqrt((a * a * a) / MU); // период, с
const incl = (97.4 + hash01(sc.id, 7) * 1.6) * DEG; // 97.499.0°
const node0 = hash01(sc.id, 3) * 360 * DEG; // долгота восх. узла @ эпоха
const u0 = hash01(sc.id, 11) * 2 * Math.PI; // фаза
// Ширина полосы обзора (км) по типу аппаратуры.
const swathKm = sc.type === "radar" ? 90 + hash01(sc.id, 5) * 110
: sc.type === "combo" ? 50 + hash01(sc.id, 5) * 70
: 24 + hash01(sc.id, 5) * 36;
return { h, a, T, incl, node0, u0, swathKm };
}
const EPOCH = (window.MissionData ? window.MissionData.RANGE_START : 0);
// Подспутниковая точка в момент t (мс).
function subPoint(p, tMs) {
const dt = (tMs - EPOCH) / 1000; // с
const u = p.u0 + 2 * Math.PI * (dt / p.T);
const lat = Math.asin(Math.sin(p.incl) * Math.sin(u)) * RAD;
let lonRel = Math.atan2(Math.cos(p.incl) * Math.sin(u), Math.cos(u));
let lon = (p.node0 + lonRel) * RAD - (360 / SIDEREAL) * dt;
lon = ((((lon + 180) % 360) + 360) % 360) - 180;
// курс (для перпендикуляра swath) численно
return { lat, lon, u };
}
// Трасса за [t0,t1] с адаптивным шагом. Возвращает сегменты (разрыв на ±180°).
function groundTrack(p, t0, t1, maxPts) {
const span = t1 - t0;
const idealStep = p.T * 1000 / 36; // ~36 точек на виток
let step = idealStep;
const cap = maxPts || 900;
if (span / step > cap) step = span / cap;
const segs = [];
let cur = [];
let prevLon = null;
for (let t = t0; t <= t1; t += step) {
const s = subPoint(p, t);
if (prevLon !== null && Math.abs(s.lon - prevLon) > 180) {
if (cur.length) segs.push(cur);
cur = [];
}
cur.push({ t, lat: s.lat, lon: s.lon });
prevLon = s.lon;
}
if (cur.length) segs.push(cur);
return segs;
}
// Точки трассы строго внутри [t0,t1] (для footprint работы), без разрыва.
function trackPoints(p, t0, t1, n) {
const pts = [];
const k = Math.max(2, n || 24);
for (let i = 0; i <= k; i++) {
const t = t0 + (t1 - t0) * (i / k);
const s = subPoint(p, t);
pts.push({ t, lat: s.lat, lon: s.lon });
}
return pts;
}
// Лента (ribbon) шириной halfKm по обе стороны трассы замкнутый полигон [lon,lat].
function ribbon(points, halfKm) {
const half = halfKm / 111; // градусы широты
const left = [], right = [];
for (let i = 0; i < points.length; i++) {
const a = points[Math.max(0, i - 1)];
const b = points[Math.min(points.length - 1, i + 1)];
const cosLat = Math.cos(points[i].lat * DEG) || 0.01;
let dx = (b.lon - a.lon) * cosLat;
let dy = (b.lat - a.lat);
const len = Math.hypot(dx, dy) || 1;
dx /= len; dy /= len;
// перпендикуляр
const px = -dy, py = dx;
const p = points[i];
left.push([p.lon + (px * half) / cosLat, p.lat + py * half]);
right.push([p.lon - (px * half) / cosLat, p.lat - py * half]);
}
return left.concat(right.reverse());
}
// Видимость станции из подспутниковой точки (грубо: по угловому расстоянию).
function nearestStation(lat, lon, stations) {
let best = null, bd = 1e9;
for (const st of stations) {
const d = Math.hypot((st.lon - lon) * Math.cos(lat * DEG), st.lat - lat);
if (d < bd) { bd = d; best = st; }
}
return { st: best, distDeg: bd };
}
// Подсолнечная точка (примитивно) для терминатора в момент tMs.
function subsolar(tMs) {
const d = new Date(tMs);
const dayMs = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
const secs = (tMs - dayMs) / 1000;
const lon = 180 - (secs / 86400) * 360; // движется на запад
const N = Math.floor((tMs - Date.UTC(d.getUTCFullYear(), 0, 0)) / 86400000);
const decl = 23.44 * Math.sin(DEG * (360 / 365) * (N - 81)); // склонение
return { lon: ((lon + 180) % 360 + 360) % 360 - 180, lat: decl };
}
// Кривая терминатора: для каждого lon широта раздела день/ночь.
function terminatorCurve(tMs) {
const ss = subsolar(tMs);
const decl = ss.lat * DEG;
const out = [];
for (let lon = -180; lon <= 180; lon += 4) {
const H = (lon - ss.lon) * DEG;
// граница: tan(lat) = -cos(H)/tan(decl)
let lat;
if (Math.abs(decl) < 0.01) lat = 0;
else lat = Math.atan(-Math.cos(H) / Math.tan(decl)) * RAD;
out.push([lon, lat]);
}
return { curve: out, nightInNorth: ss.lat < 0, subsolar: ss };
}
Object.assign(window, {
orbitParams, subPoint, groundTrack, trackPoints, ribbon,
nearestStation, subsolar, terminatorCurve, DEG, RAD,
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Some files were not shown because too many files have changed in this diff Show More