pcp-tgu-ops-ui: тест-кнопка закладки, статус SUPERSEDED, подсветка цепочки

- Тест-кнопка «Подтвердить закладку» в деталях плана для AWAITING_LAYIN
  (под подписью «ТЕСТОВЫЙ ФУНКЦИОНАЛ», с подтверждением). Гасится, если по
  КА уже заложен план со стартом в пределах ±10 мин (дублирует бэкенд-гард).
- Статус SUPERSEDED: стиль/подпись/легенда.
- Поиск уже созданного черновика — по слоту (КА + окно исходного плана),
  т.к. basePlanId черновика теперь = предшественник, а не исходный план.
- Таймлайн: при выборе плана непричастные планы гаснут, а текущий/прошлый/
  следующий обводятся яркими рамками (current #2563EB, prev #94A3B8,
  next #F59E0B). Асимметрия: prev (явная база выбранного) показывается
  всегда, даже REJECTED/SUPERSEDED; next (обратная ссылка) не подсвечивает
  мёртвые статусы — иначе вытесненная коррекция ложно числилась бы
  «следующей» у заложенного плана.
This commit is contained in:
Дмитрий Соловьев
2026-06-04 16:13:13 +03:00
parent 87905311b1
commit faadc63028
6 changed files with 162 additions and 13 deletions
@@ -15,6 +15,21 @@ import { formatLocalDate, formatLocalDateTime, formatLocalTime, parseLocal } fro
export type ZrvWindow = { startMs: number; endMs: number; kppId: string };
// Терминально-«мёртвые» статусы: план выбыл из живой цепочки (вытеснен закладкой, отклонён,
// не принят, сбой закладки, неоднозначный старт). Применяется ТОЛЬКО к «следующему» (обратная
// ссылка basePlanId → выбранный, она неоднозначна): мёртвый план — это, как правило, коррекция,
// проигравшая гонку закладки и оставшаяся со ссылкой base_plan_id на победителя (особенно старые
// строки до наследования предшественника), и без фильтра она ложно показывалась бы «следующей».
// На «предыдущий» НЕ распространяется: это явная база самого выбранного плана (forward-ссылка из
// его поля basePlanId) — её показываем всегда, даже если база REJECTED/SUPERSEDED.
const DEAD_LINK_STATUSES: ReadonlySet<string> = new Set([
"SUPERSEDED",
"REJECTED",
"LAYIN_FAILED",
"NOT_ACCEPTED",
"START_AMBIGUOUS"
]);
type ZrvBands = {
spacecraftId: string;
windows: ZrvWindow[];
@@ -207,19 +222,26 @@ export function TguTimeline({
const selected = selectedPlanId === segment.plan.planId;
// Сосед выбранного по цепочке: предыдущий (прошлое) — planId == basePlanId выбранного;
// следующий (будущее) — basePlanId == выбранный. Подсветка по реальной связи, не по времени.
// prev — явная база выбранного плана (forward-ссылка), показываем всегда, даже мёртвую.
// next — обратная ссылка; мёртвый план (вытесненный/отклонённый конкурент того же слота)
// живым продолжением не подсвечиваем.
const liveLink = !DEAD_LINK_STATUSES.has(segment.plan.status);
const neighbor = selected
? undefined
: selectedBasePlanId && segment.plan.planId === selectedBasePlanId
? "prev"
: selectedPlanId && segment.plan.basePlanId === selectedPlanId
: selectedPlanId && liveLink && segment.plan.basePlanId === selectedPlanId
? "next"
: undefined;
const labelVisible = segment.width > 92;
// При активном выборе все «непричастные» планы гасим до еле заметных, а текущий /
// прошлый / следующий обводим яркой рамкой — так видно звенья цепочки выбранного плана.
const dimmed = selectedPlanId !== undefined && !selected && neighbor === undefined;
return (
<g
key={segment.plan.planId}
className={`tgu-timeline-plan ${selected ? "is-selected" : ""}`}
className={`tgu-timeline-plan ${selected ? "is-selected" : ""} ${dimmed ? "is-dimmed" : ""}`}
onClick={() => onSelectPlan(segment.plan.planId)}
>
<title>