pcp-tgu: слоты по факту закладки, запрет ранней закладки, шире рейл редактора
- слоты (acceptedPlanSlotsUpdate) занимаются на LAID_IN, а не на ACCEPT — до закладки план ещё может быть вытеснен коррекцией - «Подтвердить закладку» недоступно, пока начало плана в будущем - левая панель редактора 15.5rem → 17rem (цифры статистики у границы)
This commit is contained in:
@@ -1032,7 +1032,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
{applyError && <div className="tgu-alert tgu-alert--danger tgu-editor-notice">{applyError}</div>}
|
{applyError && <div className="tgu-alert tgu-alert--danger tgu-editor-notice">{applyError}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tgu-editor-layout" style={{ gridTemplateColumns: rightMode === "none" ? "15.5rem minmax(0, 1fr) 0" : "15.5rem minmax(0, 1fr) 21rem" }}>
|
<div className="tgu-editor-layout" style={{ gridTemplateColumns: rightMode === "none" ? "17rem minmax(0, 1fr) 0" : "17rem minmax(0, 1fr) 21rem" }}>
|
||||||
<EditorRail
|
<EditorRail
|
||||||
selectedSpacecraftId={selectedSpacecraftId}
|
selectedSpacecraftId={selectedSpacecraftId}
|
||||||
selectedPlan={selectedPlan}
|
selectedPlan={selectedPlan}
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export function TguPlanDetails({
|
|||||||
const duration = formatDuration(plan.endMs - plan.startMs);
|
const duration = formatDuration(plan.endMs - plan.startMs);
|
||||||
// Окно решения закрылось, если с момента старта плана прошло больше grace-периода.
|
// Окно решения закрылось, если с момента старта плана прошло больше grace-периода.
|
||||||
const decisionWindowClosed = Date.now() > plan.startMs + DECISION_GRACE_MINUTES * 60_000;
|
const decisionWindowClosed = Date.now() > plan.startMs + DECISION_GRACE_MINUTES * 60_000;
|
||||||
|
// Закладку нельзя подтверждать, пока начало плана ещё в будущем: план закладывается на КА
|
||||||
|
// по факту наступления своего старта, а не заранее.
|
||||||
|
const layinStartInFuture = plan.startMs > Date.now();
|
||||||
// «Создать черновик на основе плана» (replace, окно 1:1, все включения) доступно для
|
// «Создать черновик на основе плана» (replace, окно 1:1, все включения) доступно для
|
||||||
// персистентных правимых баз. DRAFT уже имеет свои действия; PLANNED — виртуальная проекция,
|
// персистентных правимых баз. DRAFT уже имеет свои действия; PLANNED — виртуальная проекция,
|
||||||
// не база; LAID_IN — replace 1:1 запрещён (слот зафиксирован, только fork-forward).
|
// не база; LAID_IN — replace 1:1 запрещён (слот зафиксирован, только fork-forward).
|
||||||
@@ -232,9 +235,11 @@ export function TguPlanDetails({
|
|||||||
<button
|
<button
|
||||||
className="tgu-button tgu-button--accept"
|
className="tgu-button tgu-button--accept"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={decisionInFlight || layinConflict}
|
disabled={decisionInFlight || layinConflict || layinStartInFuture}
|
||||||
title={
|
title={
|
||||||
layinConflict
|
layinStartInFuture
|
||||||
|
? "Начало плана ещё не наступило — подтвердить закладку можно только после его старта"
|
||||||
|
: layinConflict
|
||||||
? "По КА уже заложен план со стартом в пределах ±10 минут — закладка недоступна"
|
? "По КА уже заложен план со стартом в пределах ±10 минут — закладка недоступна"
|
||||||
: "Сымитировать событие «план заложен» (AWAITING_LAYIN → LAID_IN)"
|
: "Сымитировать событие «план заложен» (AWAITING_LAYIN → LAID_IN)"
|
||||||
}
|
}
|
||||||
@@ -242,7 +247,12 @@ export function TguPlanDetails({
|
|||||||
>
|
>
|
||||||
Подтвердить закладку
|
Подтвердить закладку
|
||||||
</button>
|
</button>
|
||||||
{layinConflict && (
|
{layinStartInFuture && (
|
||||||
|
<div className="tgu-details__test-hint">
|
||||||
|
Начало плана ещё в будущем — подтвердить закладку можно только после наступления его старта.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!layinStartInFuture && layinConflict && (
|
||||||
<div className="tgu-details__test-hint">
|
<div className="tgu-details__test-hint">
|
||||||
По КА уже есть заложенный план со стартом в пределах ±10 минут — подтвердить нельзя.
|
По КА уже есть заложенный план со стартом в пределах ±10 минут — подтвердить нельзя.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-1
@@ -149,6 +149,10 @@ class HandlePlanDecisionUseCase(
|
|||||||
|
|
||||||
plannedPlanRepository.save(plan)
|
plannedPlanRepository.save(plan)
|
||||||
spacecraftPlanningStateJpaRepository.save(state)
|
spacecraftPlanningStateJpaRepository.save(state)
|
||||||
|
|
||||||
|
// Статусы слотов проставляем только по факту закладки на КА (LAID_IN), а не на ACCEPT:
|
||||||
|
// до закладки план ещё может быть вытеснен коррекцией, и занимать слоты рано.
|
||||||
|
acceptedPlanSlotsUpdateJobService.enqueue(plan.planId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Гасит в SUPERSEDED ещё не заложенные планы КА, чьё окно пересекается с заложенным. */
|
/** Гасит в SUPERSEDED ещё не заложенные планы КА, чьё окно пересекается с заложенным. */
|
||||||
@@ -247,9 +251,9 @@ class HandlePlanDecisionUseCase(
|
|||||||
// ACCEPT больше НЕ запирает цепочку и не двигает базу/версию: принятый план лишь ждёт
|
// ACCEPT больше НЕ запирает цепочку и не двигает базу/версию: принятый план лишь ждёт
|
||||||
// закладки. Базой (currentBasePlanId) и chainVersion управляет только LAID_IN
|
// закладки. Базой (currentBasePlanId) и chainVersion управляет только LAID_IN
|
||||||
// ([handleLayinConfirmed]) — до закладки можно отправить и принять коррекцию того же окна.
|
// ([handleLayinConfirmed]) — до закладки можно отправить и принять коррекцию того же окна.
|
||||||
|
// Статусы слотов тоже проставляются не здесь, а на закладке ([handleLayinConfirmed]).
|
||||||
planIssueAttemptRepository.save(attempt)
|
planIssueAttemptRepository.save(attempt)
|
||||||
plannedPlanRepository.save(plan)
|
plannedPlanRepository.save(plan)
|
||||||
acceptedPlanSlotsUpdateJobService.enqueue(plan.planId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rejectPlan(
|
private fun rejectPlan(
|
||||||
|
|||||||
+7
-3
@@ -61,7 +61,7 @@ class PlanIssueAndDecisionFlowTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `accepted decision moves plan to AWAITING_LAYIN and enqueues slots without anchoring base`() {
|
fun `accepted decision moves plan to AWAITING_LAYIN without anchoring base or touching slots`() {
|
||||||
val spacecraftId = "25544"
|
val spacecraftId = "25544"
|
||||||
val plan = plannedPlan(spacecraftId, time(12), time(13), PlannedPlanStatus.WAITING_DECISION)
|
val plan = plannedPlan(spacecraftId, time(12), time(13), PlannedPlanStatus.WAITING_DECISION)
|
||||||
val attempt = attempt(plan.planId, PlanIssueAttemptStatus.STARTED)
|
val attempt = attempt(plan.planId, PlanIssueAttemptStatus.STARTED)
|
||||||
@@ -80,7 +80,8 @@ class PlanIssueAndDecisionFlowTest {
|
|||||||
// ACCEPT больше НЕ делает план базой и не двигает версию — это делает только LAID_IN.
|
// ACCEPT больше НЕ делает план базой и не двигает версию — это делает только LAID_IN.
|
||||||
assertNull(state.currentBasePlanId)
|
assertNull(state.currentBasePlanId)
|
||||||
assertEquals(0, state.chainVersion)
|
assertEquals(0, state.chainVersion)
|
||||||
verify(slotsUpdateJobService).enqueue(plan.planId)
|
// Слоты проставляются на закладке (LAID_IN), а не на ACCEPT.
|
||||||
|
verify(slotsUpdateJobService, never()).enqueue(plan.planId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -95,12 +96,13 @@ class PlanIssueAndDecisionFlowTest {
|
|||||||
val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, chainVersion = 4)
|
val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, chainVersion = 4)
|
||||||
val processedRepository = mock(ProcessedPlanDecisionEventRepository::class.java)
|
val processedRepository = mock(ProcessedPlanDecisionEventRepository::class.java)
|
||||||
`when`(processedRepository.insertIfAbsent(anyNonNull(), anyNonNull())).thenReturn(1).thenReturn(0)
|
`when`(processedRepository.insertIfAbsent(anyNonNull(), anyNonNull())).thenReturn(1).thenReturn(0)
|
||||||
|
val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
|
||||||
val useCase = HandlePlanDecisionUseCase(
|
val useCase = HandlePlanDecisionUseCase(
|
||||||
processedPlanDecisionEventRepository = processedRepository,
|
processedPlanDecisionEventRepository = processedRepository,
|
||||||
planIssueAttemptRepository = attemptRepository(linkedMapOf()),
|
planIssueAttemptRepository = attemptRepository(linkedMapOf()),
|
||||||
plannedPlanRepository = plannedRepository(store),
|
plannedPlanRepository = plannedRepository(store),
|
||||||
spacecraftPlanningStateJpaRepository = stateRepository(state),
|
spacecraftPlanningStateJpaRepository = stateRepository(state),
|
||||||
acceptedPlanSlotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java),
|
acceptedPlanSlotsUpdateJobService = slotsUpdateJobService,
|
||||||
planningProperties = planningProperties()
|
planningProperties = planningProperties()
|
||||||
)
|
)
|
||||||
val message = feedbackMessage(spacecraftId, laidIn.planId)
|
val message = feedbackMessage(spacecraftId, laidIn.planId)
|
||||||
@@ -113,6 +115,8 @@ class PlanIssueAndDecisionFlowTest {
|
|||||||
assertEquals(5, state.chainVersion)
|
assertEquals(5, state.chainVersion)
|
||||||
assertEquals(PlannedPlanStatus.SUPERSEDED, competitor.status)
|
assertEquals(PlannedPlanStatus.SUPERSEDED, competitor.status)
|
||||||
assertEquals(PlannedPlanStatus.AWAITING_LAYIN, unrelated.status)
|
assertEquals(PlannedPlanStatus.AWAITING_LAYIN, unrelated.status)
|
||||||
|
// Слоты проставляются по факту закладки — ровно один раз (второй вызов идемпотентен).
|
||||||
|
verify(slotsUpdateJobService).enqueue(laidIn.planId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user