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 + тесты.
This commit is contained in:
Дмитрий Соловьев
2026-06-03 16:25:46 +03:00
parent 057452f19d
commit 9c2d8516bc
26 changed files with 1316 additions and 240 deletions
@@ -218,6 +218,45 @@ class MissionEditModesTest {
assertEquals(HttpStatus.NOT_FOUND, ex.statusCode)
}
// --- createBlankMission (план новой цепочки «без истории») ---
@Test
fun `createBlankMission persists a mission and its plan with the given window`() {
val saved = arrayOfNulls<space.nstart.pcp.pcp_mission_planing_service.entity.MissionEntity>(1)
`when`(missionRepository.save(anyNonNull())).thenAnswer { inv ->
val m = inv.arguments[0] as space.nstart.pcp.pcp_mission_planing_service.entity.MissionEntity
saved[0] = m
m
}
val start = LocalDateTime.of(2026, 6, 10, 12, 0)
val stop = LocalDateTime.of(2026, 6, 10, 12, 10)
val response = service.createBlankMission(
space.nstart.pcp.pcp_mission_planing_service.controller.MissionBlankRequest(
satelliteId = 25544, missionStart = start, missionStop = stop
)
)
assertEquals(saved[0]!!.missionId, response.newMissionId)
assertEquals(25544L, saved[0]!!.satelliteId)
assertEquals(start, saved[0]!!.missionStart)
assertEquals(stop, saved[0]!!.missionStop)
assertEquals("NEW", saved[0]!!.status)
}
@Test
fun `createBlankMission with stop not after start yields 400`() {
val t = LocalDateTime.of(2026, 6, 10, 12, 0)
val ex = assertThrows<ResponseStatusException> {
service.createBlankMission(
space.nstart.pcp.pcp_mission_planing_service.controller.MissionBlankRequest(
satelliteId = 25544, missionStart = t, missionStop = t
)
)
}
assertEquals(HttpStatus.BAD_REQUEST, ex.statusCode)
}
// --- helpers ---
private fun survey(): SurveyModeEntity {