Слайдер «Сутки» раскрывался на весь диапазон после брони: bookedSlots менялся → новый референс days → эффект сбрасывал окно, хотя набор суток тот же. Завязал сброс на сигнатуру набора суток (границы дней), а не на референс массива.
Замена авто-чипа «полоса ×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 чисто
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 — нет.
- Бронь теперь всегда через диалог подтверждения (симметрично «Снять всё»),
убран порог в 500 слотов.
- «Снять всё» дочитывает брони заявки через by-request и снимает объединение
локального и серверного наборов bookedSlotId — гарантированно вся заявка.
- Карта: забронированные слоты вне окна суток подчинены фильтрам видимости
(статус «бронь»/КА/ветвь) — при выключенном фильтре брони их не видно.
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 помечен сделанным.
- Панель фильтров видимости (статус/КА/ветвь) над строкой выбора: единый
filteredSlots сужает и таблицу, и карту; пустое состояние с «Сбросить».
- Убран потолок отрисовки карты — общую картину не режем, объём сужает оператор
фильтрами.
- Карту не перецентрируем после расчёта (был прыжок на произвольный первый слот).
- «Показать все слоты» шлёт all=true и показывает занятые бронью (BUSY); бронь
их не берёт (исключены из выбора/счётчика, чекбокс disabled).
- Починен мёртвый маппинг статуса: бэкенд шлёт BUSY, фронт ждал OCCUPIED.
- BUSY отображается как «недоступен» (конфликт с бронью), а не «занят» —
чтобы не путать с самой бронью.
«Показать все слоты» должно показывать полный набор. При all=true reqCover
запрашивает статусы [AVAILABLE, BOOKED, BUSY], а selectCoverageSlots собирает
BUSY отдельно и добавляет в ответ только в ветке без покрытия — в solver
(«Подобрать покрытие») занятые слоты по-прежнему не попадают.
При наложении в крайней точке хват попадал на верхний ползунок to, который на
правом краю заперт fromIndex и не уходит влево, а нижний from был недоступен.
Теперь при fromIndex === toIndex управляемый ползунок выбирается по направлению
движения (влево → from, вправо → to). Чинит и «Сутки» (SlotDaySelector), и
«Витки» (MapPassSelector).
Бэк (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.
По замечаниям: Крен и Статус не помещались (8 колонок на узкую панель).
- «Виток» теперь = виток / ветвь / цикл (281 / восх / 1); отдельная «Слот/цикл» убрана
- «Начало» с секундами (formatLocalDateTimeSeconds) — слоты короткие, без секунд начало=конец
- «Конец» заменён на «Длит., с» (slotDurationSec); colSpan распорок 8→7
- Крен и Статус снова видны
Тесты 70/70, build чисто.
Вкладка «Слоты» масштабирована под ~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 чисто.
- новая вкладка: заявки интервала на карте, выбор заявки кликом/инспектором,
расчёт слотов покрытия, бронирование и снятие брони
- выбор заявки без выпадающего списка (не масштабируется на ~10к): все заявки
на карте, панель справа с кнопкой «Выбрать», имя выбранной заявки в тулбаре,
кнопка «Сброс», фильтр заявок по интервалу
- бронь отмечаем по ответу POST booking/request (by-request отдаёт слот в чужом
cycle-фрейме — ключ не совпадал, бейдж «бронь» не появлялся)
- подсветка и автоскролл активной строки по клику на слот (ручной scrollBy)
- меню выбора при плотном наложении слотов (SlotPickMenu); контур заявки
pickable:false, чтобы не «съедать» меню; панель не закрывается при выборе
- фильтр отображения по суткам (SlotDaySelector)
- gateway: маршрут slots (/api/pcp-slots/**)
- карточка полосы обзора (swath): КА и виток в инспекторе
При первой правке плана ensureDraft→createDraft(replace) падал 409, если по КА
уже есть открытый черновик на это окно (осиротевший с прошлой сессии — авто-отмена
best-effort). В UI это показывалось голым «Conflict» (на стенде не подхвачен
include-message у tgu).
Теперь на 409 от createDraft редактор подхватывает существующий черновик того же
КА с тем же окном (adoptExistingDraft через fetchDraftPlans, сверка окна ±2 c) и
продолжает в нём; если подходящего нет — читаемое сообщение со ссылкой на вкладку
«Планы» вместо «Conflict».
Обобщил ComplexInfoPanel в общий MapObjectInspector (tgu-map-2d): карточки для
всех типов объектов — заявка, включение комплексного плана, ячейка тепловой карты,
маршрут planWorks, станция, маркер КА, линия; список с раскрытием при наложении.
Вкладка «Карта» переведена на enableObjectChooser + onObjectsAtPoint + правый
инспектор — заменила разрозненные Tgu2DMapRequestsPanel/Tgu2DMapStationPanel (удалены)
и объектную часть нижней плашки (Tgu2DMapDetails ужат до контекста плана). Вкладка
«Комплексный план» использует тот же инспектор.
RequestAtPoint переехал в tgu-requests/RequestDetailsPanel (нужен вкладке «Заявки»).
Чекбокс «Тепловая карта» — фоновый слой-заливка ячеек сетки заданий по важности
(cool→hot), источник GET /api/pcp-request/v1/cells/priority-map (cellsApi + drawHeatmap).
Heatmap — полноценный слой MapPolygon (layer "heatmap", importance/cellNum), участвует
в spatial-индексе и chooser, рисуется под контурами плана/заявок.
Клик по объекту открывает правый инспектор ComplexInfoPanel (по аналогии с редактором):
карточки ячейки приоритета, включения комплексного плана и заявки; при наложении —
список объектов под точкой клика с раскрытием. При включённой тепловой карте ячейка
всегда идёт первой в списке и раскрыта по умолчанию.
Бэклог #44 отмечен сделанным.
Кнопка предлагала replace-черновик для уже стартовавшего плана, хотя бэк
(CreateDraftUseCase.createReplaceDraft) возвращает 422 «окно выдачи заморожено».
Гард canCreateDraftFromPlan теперь требует startInFuture (зеркалит редактор);
для стартовавших eligible-планов вместо кнопки — amber-подсказка про fork-forward.
Запросы 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 бэклога.
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.
Дашборд «Группировка» наполняется из плана: режимы SURVEY/DROP → интервалы
съёмки и сброса с контуром и станцией, окна ЗРВ выделены в отдельную «связь»
(раньше показывались как сброс). Заполнение бортового накопителя считается
из объёмов маршрутов (volumeMb) с модельной ёмкостью 500 ГБ
(ONBOARD_CAPACITY_MB) — memAt по таймлайну, подсветка контура/линии сброса
на карте.
Бэкенд считает геометрию видимости/маршрута только в |крен| ≤ 45°,
поэтому предел снижен с 89° до 45°. Введён общий clampRoll (зажим +
округление до 0.1°), применён при ручной правке крена (кнопки ±,
прямой ввод) и при загрузке сохранённого маршрута в окно построения
(легаси-маршрут вне ±45° зажимается).
Сброс рисуется приглушённо-малиновым (rgba(214,96,138,0.82), ширина 5)
поверх трассы: контрастно к бирюзовым виткам, не сливается с заявками
и контурами съёмки.
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) — до роста требований к точности.
Объём режима = скорость × длительность, Мбайт: съёмка 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 показывает объём в редакторе (инспектор, построенный маршрут) и в сводке комплексного плана.
Тестовый эндпоинт загрузки платформ принимает root-array НСИ в теле запроса (@RequestBody List<NsiPlatformDto>) вместо пути к файлу; маршрут load-from-file переименован в load-manual.
application.yaml/yml всех сервисов и config-repo переведены с 192.168.60.x
на localhost (дефолты CONFIG_SERVER_URI и пр.); обновлены примеры в
changeIpInCurrentFolder.sh и create_pcp_databases.sh.
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).
Встраивание дизайн-хэндоффа 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-модуль
Редактор: инспектор включения теперь только просмотр (время с десятыми долями
секунды), кнопки «Редактировать»→«Удалить». «Редактировать» открывает окно
построения, предзаполненное параметрами, и при сохранении заменяет маршрут.
Прицеливание опорной точки — явный режим: активно, пока не выбран проход.
Планы: состав включений выбранного плана перечитывается при возврате на вкладку
«Планы», иначе счётчик маршрутов оставался прежним после правок в редакторе.
Новая вкладка планирования «Комплексный план» (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 в дефолтах слоёв и в редакторе/
заявках/тесте интервала.
- слоты (acceptedPlanSlotsUpdate) занимаются на LAID_IN, а не на ACCEPT —
до закладки план ещё может быть вытеснен коррекцией
- «Подтвердить закладку» недоступно, пока начало плана в будущем
- левая панель редактора 15.5rem → 17rem (цифры статистики у границы)
Вкладка «Редактор»:
- Кнопка «Рассчитать план» — запускает 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.
Индикатор «проблема КА» теперь означает НЕразрешённый отказ, а не «есть
хоть один REJECTED»: unresolvedProblemSpacecraftIds не красит КА, если
проблемный план снят с учёта (dismissedAt) либо цепочка уже продвинулась
за него (advanced-план со стартом не раньше). Исправленные и снятые
отказы гаснут сами.
- кнопка «Снять с учёта» / «Вернуть на учёт» в деталях плана для
терминально-негативных статусов + бейдж «снят с учёта»;
- dismissPlan/restorePlan (через BFF), поле dismissedAt в модели;
- снятый план приглушён и штрихуется на таймлайне;
- тесты на unresolvedProblemSpacecraftIds.
Оператор деактивирует отменённый/неудавшийся план, чтобы он не считался
проблемой и не красил индикатор КА. Косметико-статистический флаг
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).
Диалог получает initialBasePlanId — выбранный (в фокусе) план; селект
«Базовое состояние» предзаполняется им, если он годится в базу (тот же КА,
не PLANNED/DRAFT), иначе остаётся «без истории». Оператор может сменить
базу вручную.