From fd3603a0864370a685c770e4739fd8ff82885cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D1=8C=D0=B5=D0=B2?= Date: Thu, 4 Jun 2026 13:06:58 +0300 Subject: [PATCH] =?UTF-8?q?pcp-tgu:=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81?= =?UTF-8?q?=D1=8B=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B0=20=E2=80=94=20=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D1=83=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BB?= =?UTF-8?q?=D0=B5=D0=B3=D0=B5=D0=BD=D0=B4=D0=B0=20=D1=81=20=D1=82=D1=83?= =?UTF-8?q?=D0=BB=D1=82=D0=B8=D0=BF=D0=B0=D0=BC=D0=B8=20+=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B6=D0=B8=D0=B7=D0=BD=D0=B5=D0=BD=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=86=D0=B8=D0=BA=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../PCP_TGU_PLAN_STATUS_LIFECYCLE.md | 143 ++++++++++++++++++ .../src/features/tgu-planning/TguLegend.tsx | 3 +- .../src/features/tgu-planning/tguStatus.ts | 54 ++++++- .../pcp-tgu-ops-ui/src/model/timelineTypes.ts | 2 + .../src/styles/tgu-planning.css | 17 ++- .../service/HandlePlanDecisionUseCase.kt | 7 + 6 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md diff --git a/docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md b/docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md new file mode 100644 index 0000000..c2402c5 --- /dev/null +++ b/docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md @@ -0,0 +1,143 @@ +# Жизненный цикл статусов плана ТГУ + +Справочник по статусам 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 (цепочка на паузе) + │ + ┌────────────────────────────────┼────────────────────────────────┐ + событие «заложен» событие «не заложен» событие «не принят» + (пока не реализовано) ▼ ▼ + ▼ LAYIN_FAILED NOT_ACCEPTED + LAID_IN (срочная перевыдача) (срочная перевыдача) + + при сбое старта 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`). +- **Что значит.** План принят и **оптимистично считается заложенным**; это текущий **базовый план + цепочки** (`state.currentBasePlanId = plan.planId`, `chainVersion += 1`). Запускается джоб + обновления слотов принятого плана (`acceptedPlanSlotsUpdateJobService.enqueue`). +- **Дальше.** При подтверждении закладки → `LAID_IN` (терминальный успех; **событие пока не + реализовано**, см. ниже). При негативе — `LAYIN_FAILED`/`NOT_ACCEPTED`. +- В легенде это де-факто «принятый/заложенный» статус (зелёный). Перевыдача в свой слот ещё + возможна, пока до старта больше `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`. +- **Что значит.** Подтверждённая закладка, терминальный успех; «точка невозврата» — дальше план + меняется только через fork-forward (используется в гардах `CreateDraftUseCase`, + `assertReissuableInSlot`). +- **⚠️ TODO — не реализовано.** Потребителя события «план заложен» сейчас **нет**, поэтому план в + `LAID_IN` не переходит и остаётся в `AWAITING_LAYIN`. Нужно: Kafka-consumer (по аналогии с + `LayinFailedConsumer`/`PlanNotAcceptedConsumer`) + `handleLayinConfirmed` в + `HandlePlanDecisionUseCase` (`AWAITING_LAYIN → LAID_IN` по текущему базовому плану). Маркер-TODO + стоит в коде `HandlePlanDecisionUseCase` рядом с обработчиками негативного фидбека. +- В легенде статус показан (зелёный, темнее `AWAITING_LAYIN`) — заранее, чтобы при появлении события + цвет уже был. + +## Зарезервированные / не возникающие сейчас статусы + +- **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) diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguLegend.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguLegend.tsx index c4b6fac..b3a9b97 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguLegend.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-planning/TguLegend.tsx @@ -4,12 +4,11 @@ export function TguLegend() { return (
{STATUS_STYLES.map((item) => ( - + {item.label} ))} - Стрелки показывают следующий план того же КА по времени.
); } diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-planning/tguStatus.ts b/services/pcp-tgu-ops-ui/src/features/tgu-planning/tguStatus.ts index 368c05b..c9a4b31 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-planning/tguStatus.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-planning/tguStatus.ts @@ -1,14 +1,51 @@ import type { TguPlanStatus } from "../../model/tguTypes"; import type { TimelineStatusStyle } from "../../model/timelineTypes"; +// Порядок — по жизненному циклу плана (см. docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md). +// ACCEPTED из enum бэка здесь намеренно отсутствует: приёмка переводит план сразу в AWAITING_LAYIN. +// LAID_IN показан, хотя пока не возникает — нужен обработчик события подтверждения закладки +// (AWAITING_LAYIN → LAID_IN), см. TODO в HandlePlanDecisionUseCase и док жизненного цикла. export const STATUS_STYLES: TimelineStatusStyle[] = [ - { status: "ACCEPTED", label: "ACCEPTED", color: "#2f9e44", className: "status-accepted" }, - { status: "REJECTED", label: "REJECTED", color: "#d64545", className: "status-rejected" }, - { status: "WAITING_DECISION", label: "WAITING_DECISION", color: "#f0ad2e", className: "status-waiting" }, - { status: "PLANNED", label: "PLANNED", color: "#8d99a6", className: "status-planned" }, - { status: "ISSUING", label: "ISSUING", color: "#2f80ed", className: "status-issuing" }, - { status: "START_AMBIGUOUS", label: "START_AMBIGUOUS", color: "#b42318", className: "status-ambiguous" }, - { status: "DRAFT", label: "ЧЕРНОВИК", color: "#a98bdb", className: "status-draft" } + { + status: "DRAFT", label: "ЧЕРНОВИК", color: "#a98bdb", className: "status-draft", + description: "Черновик: план в стадии правки, в выдачу пока не идёт." + }, + { + status: "PLANNED", label: "PLANNED", color: "#8d99a6", className: "status-planned", + description: "Возможный будущий план (проекция цепочки)." + }, + { + status: "ISSUING", label: "ISSUING", color: "#2f80ed", className: "status-issuing", + description: "Идёт выдача плана." + }, + { + status: "WAITING_DECISION", label: "WAITING_DECISION", color: "#f0ad2e", className: "status-waiting", + description: "План выдан, ожидается решение (принять / отклонить)." + }, + { + status: "AWAITING_LAYIN", label: "AWAITING_LAYIN", color: "#2f9e44", className: "status-awaiting-layin", + description: "План принят, ожидается подтверждение закладки." + }, + { + status: "LAID_IN", label: "LAID_IN", color: "#1b6b34", className: "status-laid-in", + description: "Получено подтверждение закладки плана." + }, + { + status: "START_AMBIGUOUS", label: "START_AMBIGUOUS", color: "#b42318", className: "status-ambiguous", + description: "Ошибка после старта процесса выдачи плана. Требуется оператор." + }, + { + status: "REJECTED", label: "REJECTED", color: "#d64545", className: "status-rejected", + description: "План отклонён, цепочка поставлена на паузу." + }, + { + status: "NOT_ACCEPTED", label: "NOT_ACCEPTED", color: "#e8590c", className: "status-not-accepted", + description: "План не принят, идёт срочная перевыдача на ближайшую ЗРВ." + }, + { + status: "LAYIN_FAILED", label: "LAYIN_FAILED", color: "#a61e4d", className: "status-layin-failed", + description: "План не заложен, идёт срочная перевыдача на ближайшую ЗРВ." + } ]; const STATUS_BY_NAME = new Map(STATUS_STYLES.map((style) => [style.status, style])); @@ -19,7 +56,8 @@ export function statusStyle(status: TguPlanStatus): TimelineStatusStyle { status, label: status || "UNKNOWN", color: "#79828d", - className: "status-unknown" + className: "status-unknown", + description: "Неизвестный статус." } ); } diff --git a/services/pcp-tgu-ops-ui/src/model/timelineTypes.ts b/services/pcp-tgu-ops-ui/src/model/timelineTypes.ts index 8db7416..592a864 100644 --- a/services/pcp-tgu-ops-ui/src/model/timelineTypes.ts +++ b/services/pcp-tgu-ops-ui/src/model/timelineTypes.ts @@ -59,4 +59,6 @@ export type TimelineStatusStyle = { label: string; color: string; className: string; + /** Человеческое пояснение статуса — тултип в легенде. */ + description: string; }; diff --git a/services/pcp-tgu-ops-ui/src/styles/tgu-planning.css b/services/pcp-tgu-ops-ui/src/styles/tgu-planning.css index 361dae6..aafc25c 100644 --- a/services/pcp-tgu-ops-ui/src/styles/tgu-planning.css +++ b/services/pcp-tgu-ops-ui/src/styles/tgu-planning.css @@ -150,6 +150,7 @@ align-items: center; gap: 0.35rem; white-space: nowrap; + cursor: help; } .tgu-legend__swatch { @@ -159,18 +160,26 @@ background: #79828d; } -.tgu-legend__note { - color: var(--accent-strong); +.status-awaiting-layin { + background-color: #2f9e44; } -.status-accepted { - background-color: #2f9e44; +.status-laid-in { + background-color: #1b6b34; } .status-rejected { background-color: #d64545; } +.status-not-accepted { + background-color: #e8590c; +} + +.status-layin-failed { + background-color: #a61e4d; +} + .status-waiting { background-color: #f0ad2e; } diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt index 1171cdc..a8f71b5 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt @@ -82,6 +82,13 @@ class HandlePlanDecisionUseCase( } } + // TODO(layin-confirmed): реализовать приём события «план заложен» (положительный фидбек по закладке). + // Сейчас принятый план остаётся в AWAITING_LAYIN (оптимистично «заложен») и в LAID_IN никогда не + // переходит — нет потребителя этого события. Нужно: Kafka-consumer (по аналогии с LayinFailedConsumer) + // + handleLayinConfirmed, который по текущему базовому плану в AWAITING_LAYIN переводит его в + // PlannedPlanStatus.LAID_IN (терминальный успех, «точка невозврата»: дальше только fork-forward). + // См. docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md. + /** Событие «не заложен» по базовому плану — срочная перевыдача на ближайшую ЗРВ. */ @Transactional fun handleLayinFailed(message: PlanExecutionFeedbackMessage) {