7c10f9a10d
- бэклог-pcp-tgu-ops-ui: #50 (M1/M2/M3 слой поддержки решений), #51 (вызовы через gateway), #52/#53 (находки /review), #49 (баг обновления BUSY после снятия брони), раздел «Выполнено» (чистки по /review). - docs/tasks/TASK_slot-strip-selection: дизайн-план и артефакты /autoplan по выбору полосами; отметка о реализации it.1.
136 lines
53 KiB
Markdown
136 lines
53 KiB
Markdown
# Бэклог pcp-tgu-ops-ui
|
||
|
||
Список задач по сервису **pcp-tgu-ops-ui** (и связанным бэкендам).
|
||
Ведётся вручную и с помощью Claude: по просьбе — отмечаем выполненное и добавляем новые задачи.
|
||
|
||
**Обозначения статуса:**
|
||
- `[ ]` — не начато
|
||
- `[~]` — в работе
|
||
- `[x]` — выполнено (переносится в раздел «Выполнено» с пометкой)
|
||
|
||
У каждой задачи есть стабильный номер `#N` — он не меняется при правках списка, на него удобно ссылаться. Новые задачи получают следующий свободный номер.
|
||
|
||
> Задачи #12–#25 заведены по результатам аудита. Подробности, обоснование и рекомендации
|
||
> по каждой — в [аудит-проекта-2026-06-06.md](аудит-проекта-2026-06-06.md) (ссылки на коды находок B/F/R/C).
|
||
|
||
_Обновлено: 2026-06-16_
|
||
|
||
---
|
||
|
||
## Активные задачи
|
||
|
||
### Карта и отображение
|
||
- [ ] **#1** — Отображать трассы полос исходя из возможности съёмки КА. Для оптики показывать только участки, попадающие в допустимый угол Солнца.
|
||
- [x] **#2** — При клике в область, где пересекается несколько полос обзора, выводить список этих объектов на правой панели; при выборе элемента — выделять соответствующий объект на карте. _Сделано: на вкладке «Редактор» (где рисуются полосы обзора) клик собирает полосы обзора отдельно от прочих объектов — если под точкой нет иного интерактивного объекта (заявки/маршрута/сброса/станции), а полос обзора несколько (наложение соседних витков), они отдаются списком в правую панель (`MapPickPanel`), как у прочих наложений; одиночная полоса — сразу карточка. Выбор/раскрытие элемента подсвечивает полосу на карте (`selectedObjectId`→`mapFocus.objectId`). Полоса обзора получила карточку (КА + виток): новые поля `revolution`/`spacecraftId` у swath-полигона, проброс в `Tgu2DMapSelection`, ветка swath в `MapInfoPanel`. Клик по заявке/маршруту поверх полос не засоряется полосами (полосы — только когда другого объекта под точкой нет). Файлы: `Tgu2DMapView.handleEditorClick`, `mapTypes.ts`, `mapSelectionTypes.ts`, `TguEditorTab` (сборка сцены), `EditorPanels.MapInfoPanel`._ _Доработка (2026-06-11): когда под точкой есть наложение ≥2 интерактивных объектов (напр. маршрут + заявка), в список выбора теперь добавляются и лежащие под кликом полосы обзора, чтобы их данные тоже были видны. Список упорядочен по приоритету (`editorHitRank`): маршрут съёмки → заявка → полоса обзора (маркеры/станции — перед маршрутом, сбросы — между заявкой и полосой). Одиночный клик по маршруту/заявке по-прежнему сразу выбирает объект (без списка), иначе любой клик в редакторе превращался бы в список из-за сплошного покрытия полосами._
|
||
- [ ] **#3** — Показ забронированных слотов на вкладках «Карта» и «Редактор». Включает **подгрузку** данных по забронированным слотам с бэкенда (фетч), а не только отрисовку уже имеющихся. На вкладке «Редактор» они должны быть другого цвета и выделяться.
|
||
|
||
### Группировка
|
||
- [ ] **#36** — Заполнение бортового накопителя на вкладке «Группировка». Сейчас (MVP) считается на фронте из объёмов маршрутов плана (`volumeMb`): старт 0 на начало интервала плана, `+vol` на конце съёмки, `−vol` на конце сброса, ёмкость борта — модельные 500 ГБ (`ONBOARD_CAPACITY_MB` в `features/constellation/realData.ts`). Довести до реального источника: брать ёмкость борта из НСИ платформы (не хардкод), а заполнение — из телеметрии/бортового учёта, когда такой канал появится; уточнить модель «съёмка копит / сброс опустошает» (частичные сбросы, очерёдность).
|
||
|
||
### Редактор
|
||
- [ ] **#4** — Показывать на вкладке «Редактор» только те заявки, которые может удовлетворить аппарат (например, для оптики не показывать заявку, для которой доступна только локация).
|
||
- [ ] **#5** — Дать возможность превратить план в статусе `PLANNED` (построенный цепочкой от последнего `LAID_IN`) в черновик и вручную наполнить его включениями — до того, как он будет автоматически выдан по таймеру.
|
||
- [ ] **#38** — Правка SLOTS-маршрута «телепортирует» его (исчезает с карты). При «Редактировать» редактор пересобирает контур через `survey-route/build`, передавая сохранённый `timeStart`. Для маршрутов `source=MANUAL` это корректно (их `timeStart` = время построения, контур воспроизводится). Для `source=SLOTS` `timeStart`/виток/контур посчитаны другим конвейером и не согласованы: напр. режим 174 хранит `timeStart=2026-06-07T01:51:38`, виток 152, контур над Чёрным морем (центр 45.42, 39.40), но в 01:51:38 спутник по flight-line над Индией (12–16° с.ш., виток 150) — пересборка даёт контур над Индией (10.73, 77.06), исходный прячется → маршрут «пропадает» из вида. Корень — та же баллистическая рассинхронизация, что и [#37]. _Решение (2026-06-08): чиним только на бэкенде — согласовать `timeStart`/виток/контур у SLOTS-режимов, чтобы `survey-route/build` воспроизводил их. UI-страховку (не прятать исходный контур / блокировать правку SLOTS) сознательно НЕ делаем — симптом остаётся до бэкенд-фикса._
|
||
|
||
### Комплексный план
|
||
- [ ] **#6** — Пересчитывать комплексный план при бронировании новых слотов или при поступлении новой заявки.
|
||
- [x] **#44** — Добавить на вкладку «Комплексный план» просмотр тепловой карты (приоритеты/покрытие ячеек). Источник — `priority-map` сервиса заданий (importance ячеек) и/или статистика покрытия прогона; отрисовать как heatmap-слой поверх карты. _Сделано: чекбокс «Тепловая карта» в тулбаре; фоновый слой-заливка ячеек по важности (cool→hot) под контурами плана/заявок; источник `GET /api/pcp-request/v1/cells/priority-map`, не интерактивен (в spatial-индекс не попадает). `cellsApi.ts` + `drawHeatmap.ts` + слой `heatmap` в `Tgu2DMapView`. Клик по объекту (ячейка/включение/заявка) показывает его данные в правом инспекторе `ComplexInfoPanel` (по аналогии с редактором; при наложении — список под точкой клика с раскрытием)._
|
||
|
||
### Бэкенд / инфраструктура
|
||
- [ ] **#7** — Перевести сервис станций на ordinis (по примеру получения аппаратов в сервисе tgu).
|
||
- [ ] **#47** — Перевести сервисы с Jackson 2 на Jackson 3 и убрать двойной стек. Под Spring Boot 4.0.2 MVC уже десериализует входящий JSON через **Jackson 3** (`tools.jackson.*`), а весь код монорепы написан под **Jackson 2** (`com.fasterxml.jackson`, `versions.jackson_databind=2.18.2`); в `pcp-request-service` для внутреннего парсинга держится отдельный Jackson 2-бин (`JacksonConfig`). Из-за этой рассинхронизации словили баг: Jackson 3 по умолчанию включает `FAIL_ON_NULL_FOR_PRIMITIVES`, и явный `null` на примитивном `Double coverageRequiredPercent` ронял создание заявки (контракт-тесты не ловили — они на Jackson 2). Срочности/EOL-риска нет, это техдолг и консистентность.
|
||
- **Сейчас, дёшево (часы):** системно снять класс багов «null на примитиве» — выставить `spring.jackson.deserialization.fail-on-null-for-primitives=false` в config-repo request-service (проверить, что свойство применяется к Jackson 3-бину; если нет — через `Jackson3ObjectMapperBuilderCustomizer`). Не латать каждое поле по очереди.
|
||
- **Среднесрок, по сервису:** мигрировать по одному, пилот — `pcp-request-service` (~7 файлов main, ~0.5–1 день). Аннотации **не трогаем** — `jackson-annotations` в Jackson 3 остались в пакете `com.fasterxml.jackson.annotation` (у нас это основная масса: `@JsonIgnoreProperties` ×36, `@JsonProperty` ×16, `@JsonInclude` ×15…). Менять только databind-поверхность: `ObjectMapper` ×23, `TypeReference` ×8, `JsonNode` ×5, `jacksonObjectMapper()` ×12 → пакеты `tools.jackson.databind/core/module.kotlin`; в Jackson 3 `JsonProcessingException` стала unchecked. Дропнуть Jackson 2-зависимости и Jackson 2-бин `JacksonConfig`.
|
||
- **Главный риск — Kafka:** `KafkaConsumerConfig`/сериализаторы завязаны на `com.fasterxml` ObjectMapper. Перед стартом — спайк (~полдня): умеет ли `spring-kafka` в нашем Boot 4 Jackson 3-сериализаторы. Если нет — Kafka-слой мигрируем отдельно, HTTP/внутренний JSON переводим независимо.
|
||
- **Масштаб (src/main):** 67 файлов в 10 сервисах — pcp-dynamic-plan-service 22, pcp-ui-service 16 (выводится из эксплуатации, ср. #40 — можно не мигрировать), pcp-request-service 7, slots-service/pcp-tgu-service по 5, остальные 1–4. **Грубая оценка всей репы — ~6–10 человеко-дней**, основной риск — Kafka. Big-bang не нужен: шаг 1 закрывает причину бага, полную миграцию вести фоново сервис за сервисом.
|
||
- [ ] **#45** — Собрать native-образ сервиса (Kotlin/Native, `kompile`) и контейнер на его основе. Цель — быстрый старт и меньший footprint в сравнении с JVM-образом. Уточнить: какой сервис(ы) переводим первым, совместимость с рефлексией/AOT-конфигом (reachability metadata), сборку в CI и базовый образ контейнера.
|
||
- [ ] **#51** — Распространить правило «межсервисные вызовы только через gateway» на остальные сервисы. Для `slots-service` уже сделано (config-only): добавлен маршрут `satellite-catalog` в `pcp-gateway-service.yaml`, а `settings.request-service`/`earth-grid-service`/`satellite-catalog-service` переведены на `${pcp.services.gateway}/api/pcp-<x>` (код уже строит `$base/<path>`, gateway снимает префикс `StripPrefix=2`). Аналогично проверить и перевести прочие сервисы, которые ходят к чужим API напрямую по `pcp.services.<x>` (кандидаты: mission-planing, tgu, complex-mission, dynamic-plan и т.п.) — для каждого целевого API завести gateway-маршрут `/api/pcp-<x>/**` и репойнтить их `settings.*` на gateway. Заодно решить, нужен ли маршрут для тех целей, что сейчас вызываются прямо, но не проксируются (route-processing, coverage-scheme, stations уже без StripPrefix).
|
||
- [ ] **#37** — Рассинхрон номеров витков между баллистическими источниками. Для одного и того же КА/времени `flight-line` (трасса, `/api/tgu-planning/spacecraft/{norad}/flight-line`) и `survey-route/build` (контур, pcp-mission-planing) отдают **разный номер витка**: на Emissio 1 (norad 2) 2026-06-06 12:50–12:55 трасса помечена витком 142, а контур/сохранённые режимы — витком 144 (по широте ~74° с.ш. согласованы, расходится только нумерация). В редакторе карта фильтрует трассу/полосу обзора по видимому витку (`buildEditorMapScene` → `visibleRevolutions`), поэтому при сужении селектора до «виток N» точки трассы с другим номером выпадают и пропадает орбитальный контекст. Свести нумерацию витков к единому источнику (или маппинг на границе), чтобы трасса и маршрут согласовывались по витку.
|
||
|
||
### Проверка сервисов
|
||
- [ ] **#27** — Проверить сервис получения паспорта маршрута по отдельности.
|
||
- [ ] **#28** — Проверить связь сервиса заданий и сервиса получения паспорта маршрута между собой.
|
||
|
||
### Бэкенд / вывод pcp-ui-service из эксплуатации
|
||
- [~] **#40** — Перевести все запросы фронта с `pcp-ui-service` на источники и вывести сервис из эксплуатации. `pcp-ui-service` — наследие старого server-rendered UI; новый фронт ходит к доменным сервисам через gateway, и `pcp-ui-service` остаётся в маршрутизации только как **catch-all** для двух поверхностей: `tguApi.ts` → `/api/tgu-planning/*` (+ flight-line) и `editorApi.ts` → `/api/current-plans/missions/{id}/modes`. Карта запросов, источники, реальные пути и пошаговый план — в [план-перевода-с-pcp-ui-service.md](план-перевода-с-pcp-ui-service.md). Ключевые моменты: (1) перенести во фронт UX-перевод ошибок решения по плану (сейчас живёт в `TguPlanningService`); (2) `decision`/`confirm-layin` идут в тест-контроллер tgu (`/test/tgu/*`, гейт `tgu.test-controller.enabled`) — поведение не меняем; (3) DTO проксируются как есть, схема фронта не ломается; (4) внешних потребителей у сервиса нет (проверено grep), но перед удалением кода свериться по мёртвым поверхностям (bookings/catalog/rva/com-plan/…). После переезда — снять `ui-catch-all` из `config-repo/pcp-gateway-service.yaml`, убрать сервис из docker-compose/helm/CI/config-repo.
|
||
- [x] Шаги 1–7: репойнт фронта на источники (`tguApi.ts` → `/api/pcp-tgu` + `/test/tgu/*`; flight-line → `ballisticsApi.ts`; `editorApi.ts` modes → `/api/pcp-mission`) и снятие `ui-catch-all` из gateway. Перевод ошибок уже жил во фронте (`errorMessage` в `tguApi.ts`). Typecheck чист.
|
||
- [ ] Шаг 8: вывод сервиса из эксплуатации (compose/helm/CI/config-repo/реестр `pcp.services.ui`) и удаление кода — **отложено** до проверки репойнта на живом стенде. Пока сервис задеплоен, но без маршрутов.
|
||
|
||
### Слоты (вкладка «Слоты»)
|
||
- [x] **#41** — Запрос слотового покрытия по заявке. _Сделано (2026-06-11): добавлена отдельная вкладка «Слоты» (вместо размещения на «Заявке»/«Редакторе» — операции со слотами объёмные, их собрали в одном рабочем месте). Оператор выбирает заявку, интервал и фильтры (ветвь витка, признак «Покрытие») и жмёт «Рассчитать слоты» → слоты-кандидаты от slots-service напрямую через gateway (`GET /api/pcp-slots/api/slots/request-cover`, минуя умирающий `pcp-ui-service`, ср. #40). Результат — таблица (КА, виток/ветвь, слот/цикл, время, крен, статус) и отрисовка контуров на 2D-карте поверх контура заявки; клик по строке центрирует карту и подсвечивает контур. Новый роут gateway `id: slots` (`/api/pcp-slots/**`, StripPrefix=2 → slots-service). Файлы: `config-repo/pcp-gateway-service.yaml`, `api/slotsApi.ts`, `model/slotTypes.ts`, `features/tgu-slots/TguSlotsTab.tsx`, регистрация вкладки в `TguActiveTab`/`TguToolbar`/`TguPlanningPage`, стили `.tgu-slots-*`._
|
||
- [x] **#42** — Бронирование слотов по заявке. _Сделано (2026-06-11): на вкладке «Слоты» чекбоксами выбираются слоты → «Забронировать» (пакетно, `POST /api/slots/booking/request` с `{requestId, slots:[{satelliteId, slotNum, cycle}]}`) и «Снять бронь» (`DELETE …/booking/request?requestId=`). После операции перечитываются брони заявки (`GET …/booking/by-request/{id}`), забронированные слоты получают бейдж «бронь» в таблице и отдельный цвет (слой `complexPlan`) на карте. Связано с показом забронированных слотов (#3) и пересчётом комплексного плана при бронировании (#6) — последнее пока на бэкенде не триггерится. Уточнить при необходимости идемпотентность повторной брони._
|
||
- [x] **Фильтр отображения слотов по суткам** _(2026-06-11): на вкладке «Слоты» добавлен ползунок-фильтр «Сутки» — тот же двуручейный селектор, что «Витки» на «Редакторе» (классы `map-pass-selector*`), наложенный на карту. Слоты группируются по локальным суткам начала (`tn`); выбранный диапазон суток сужает и таблицу, и сцену карты (кандидаты), «Выбрать все» работает по видимым слотам. Новый расчёт раскрывает фильтр на весь диапазон. Клик по слоту на карте теперь не только подсвечивает строку, но и подскроливает её в зону видимости таблицы. Файлы: `features/tgu-slots/SlotDaySelector.tsx` (новый), `features/tgu-slots/TguSlotsTab.tsx`._
|
||
- _Доп. (2026-06-11): подсветка активной строки сделана явной (заливка + левый акцентный бар + bold). Скролл к строке — ручной `scrollBy` по `getBoundingClientRect` с центрированием и плавной анимацией: `scrollIntoView({block:"nearest"})` оказался ненадёжен (выбирал не тот скролл-предок / не двигался). Строка ищется по стабильному `data-slot-id`, а не по `ref` (тот при «переезде» между строками мог занулиться). Подсветку ведут только клики по слоту; клик по контуру заявки её не сбрасывает, клик по пустой карте — снимает._
|
||
- [x] **Выбор заявки на карте (вместо списка)** _(2026-06-11): на вкладке «Слоты» убран выпадающий список заявок — он не масштабируется на тысячи заявок. Базово все заявки интервала отображаются на карте; клик по заявке (или по наложению нескольких — список с раскрытием) открывает плавающую панель-инспектор справа над картой (общий `MapObjectInspector`) с данными заявки и кнопкой «Выбрать». До выбора в тулбаре вместо списка — имя выбранной заявки или «—», а левая панель показывает фильтр «Заявки на интервал» (С/По + «Загрузить»/«Сегодня»/«7 дней»/«30 дней», как на «Карте») и подсказку «Кликните заявку на карте». После выбора заявки появляется кнопка «Сброс» справа над картой — возвращает окно в состояние «заявка не выбрана, слоты не показаны». Файлы: `features/tgu-slots/TguSlotsTab.tsx`, `features/tgu-map-2d/MapObjectInspector.tsx` (опц. `onSelectRequest` → кнопка «Выбрать» в карточке заявки), стили `.tgu-slots-request*`/`.tgu-slots-reset`/`.tgu-slots-inspector`/`.tgu-slots-requests-panel`._
|
||
- [x] **Бронь не отражалась в UI + явная подсветка строки** _(2026-06-11): «не работает кнопка бронь» — диагностировано: POST `booking/request` бронирует корректно и в ответе `BookedSlotDTO` несёт верный `cycle`, но `GET booking/by-request/{id}` отдаёт **базовый** слот в другом cycle-фрейме (`bookedSlot.slot.toDTO` без сдвига на `bookedSlot.cycle`), из-за чего ключ `sat:slotNum:cycle` не совпадал с рассчитанной строкой → бейдж «бронь» не появлялся, на карте — лишний полигон. Фронт-обход: после брони отмечаем забронированными ровно те рассчитанные слоты (с контуром), что вернул POST; by-request на горячем пути не перечитываем; cancel чистит локально. Заодно подсветка активной строки слота сделана явной (фон + левый акцентный бар + bold). Root-fix (на бэке, отдельно): `slots-service SlotService.bookedByReqId/bookedById` должны возвращать слот, сдвинутый на `bookedSlot.cycle` (как `resolveBookedSlotStartTime`/перечисление в `request-cover`) — иначе пред-существующие брони не коррелируют с расчётом. Файлы: `features/tgu-slots/TguSlotsTab.tsx`, стили `.tgu-slots-table tbody tr.is-active`._
|
||
- [ ] **#43** — Уточнение статуса слота. На вкладке «Слоты» уже показывается базовый статус слота (`state`: свободен/бронь/занят) и бейдж брони по заявке. Осталось: подтягивать/обновлять актуальный статус занятости по **заложенному** плану (слоты занимаются по `LAID_IN`, а не по `ACCEPT`, ср. #10) и согласовать его источник со slots-service (real-time/после закладки), а также прокинуть статус на вкладки «Карта»/«Редактор» (#3).
|
||
- [x] **#48** — (баг, бэкенд) Слоты бронируются «не на те даты». _Исправлено 2026-06-15:_ `BookedSlotEntity.toViewDTO()` (единственный билдер за `GET …/booking/by-request/{id}`) отдавал `tn/tk/revolution` из **базового** cycle-фрейма без сдвига, тогда как все остальные пути (`toDetails`, `allByInterval`→`shiftedToCycle`, `resolveBookedSlotStartTime`) сдвигают на `cycle*durationCalc`. Сдвиг перенесён в `SlotService.bookedByReqId` (новый `toShiftedViewDTO(satellite)`, КА резолвится из каталога; нет в каталоге → базовый фрейм без падения), базовый билдер в сущности удалён. Регрессия: `SlotServiceCancelTest` (cycle=2 → +32 сут / +486 витков; fallback без КА). POST `booking/request` сохраняет только ключ (`bookedSlotId/sat/slotNum/cycle`), tn/tk не пересчитывает — корректно. <br>_Исходный отчёт:_ При бронировании набора слотов фактическое время брони расходится с тем, что оператор видел/выбирал в таблице расчёта. Корень — та же рассинхронизация cycle-фрейма, что разобрана в задаче «Бронь не отражалась в UI» (#42): `GET …/booking/by-request/{id}` отдаёт слот в **базовом** cycle-фрейме (`bookedSlot.slot.toDTO` без сдвига на `bookedSlot.cycle`), тогда как `request-cover` перечисляет слоты со сдвигом — поэтому время/сутки брони, восстановленные из by-request, не совпадают с рассчитанным слотом. Фронт сейчас обходит это, отмечая бронь по ответу POST (там cycle верный) и не перечитывая by-request на горячем пути, но это лишь маскирует симптом: пред-существующие брони и брони после перезагрузки страницы по-прежнему могут показываться/трактоваться с неверной датой. Root-fix на бэке: `slots-service SlotService.bookedByReqId/bookedById` должны возвращать слот, сдвинутый на `bookedSlot.cycle` (как `resolveBookedSlotStartTime` / перечисление в `request-cover`), чтобы время брони было согласовано с расчётом. Проверить также, что POST `booking/request` сохраняет именно те tn/tk, что соответствуют выбранному слоту, а не пересчитывает их в базовом фрейме.
|
||
|
||
- [ ] **#49** — (баг, фронт) После снятия брони статусы «недоступен» не обновляются. После снятия бронирования со слотов соседние слоты, помеченные «недоступен» (`BUSY`/band — заблокированы из-за пересечения с бронью или длины цепочки), остаются недоступными, пока оператор вручную не нажмёт «Показать все слоты». Причина: снятие брони чистит только локальные отметки брони, а «недоступность» вычисляется бэком при перечислении (`request-cover`/`all=true` с учётом соседних броней) и без повторного запроса не пересчитывается. Нужно после успешного снятия инвалидировать/перечитать затронутые слоты (или пересчитать `state` локально), чтобы освободившиеся «недоступен» сразу становились «свободен» без ручного «Показать все слоты». Файлы: `features/tgu-slots/TguSlotsTab.tsx` (хэндлеры снятия `runCancel`/`performCancelAll`), `api/slotsApi.ts`.
|
||
|
||
- [ ] **#50** — Ручное бронирование: слой поддержки решений оператора. Маршрут ручного бронирования уже есть (#41/#42), но оператор проживает его вслепую: нет сигнала «достаточно ли покрытия», отбор галочками не масштабируется на тысячи слотов, конфликты (BUSY) не объяснены. Кладём поверх маршрута информационный слой поддержки решений — «достаточно» по-прежнему судит оператор сам (без гейтов, в линии WAITING_DECISION/REJECTED). Подход — frontend-геометрия (union/intersect контуров на клиенте, без раундтрипов; точность вторична, раз судит человек). Дизайн-док: `~/.gstack/projects/missionis-observatio-terrae/dasolovyev-feat-tgu-slots-filters-design-20260616-122013.md`. Прогоняется по цепочке `/plan-ceo-review` → `/plan-design-review` → `/plan-eng-review` → код → `/review` → `/qa`.
|
||
- [ ] **M1 — ядро покрытия (frontend, без бэка):** новый `features/tgu-slots/coverage.ts` (площадь заявки, покрытие выбранного набора; юнит-тесты по образцу `slotFilters.test.ts`), `CoveragePanel` в шапке списка (живой % + бар + цель заявки справочно), слой `coverage` на карте (закрашенный накопленный след union контуров, заклипанный полигоном заявки). Лечит «достаточность». Новая npm-зависимость геометрии (`polygon-clipping` + `@turf/area` или `@turf`-subset — решить на M1). Пересчёт дебаунсом (~100 мс), union только по выбранному набору.
|
||
- [ ] **M2 — эргономика (frontend):** сортировка списка «по вкладу в покрытие» (жадный маргинальный прирост, по кнопке) + по крену/Солнцу. Лечит объём — набор собирается кратно меньшим числом кликов.
|
||
- [ ] **M3 — конфликты (backend + frontend):** обогатить BUSY-слот держателем (`bookedByReqId` уже есть в slots-service + имя/важность заявки) в `SlotDTO`/`reqCover(all=true)`, бейдж конфликта `занят: <заявка> (важн. K)` в строке. Лечит осознанное разруливание. Единственное изменение бэка.
|
||
|
||
> Задачи #52–#53 — по результатам `/review` ветки `feat/tgu-slots-filters` (2026-06-16). Замечания информационные, лендингу не мешают.
|
||
- [ ] **#52** — (производительность, бэкенд) `EarthService.requestHolders` грузит весь каталог заявок на каждый `request-cover` с `all=true`. Чтобы разрешить имя/важность горстки заявок-держателей BUSY-слотов (M3), метод постранично выкачивает **все** заявки (`for page in 1 until firstPage.totalPages`) и фильтрует их в памяти — O(всех заявок) по сети и памяти на каждый обзор с BUSY-держателями. Сейчас деградация безопасна (`runCatching → emptyMap`: при недоступности request-service бейдж показывает только id), но операция будет линейно замедляться с ростом БД заявок. Решение: добавить в `pcp-request-service` выборку заявок **по списку id** (batch-by-ids) и звать её из `requestHolders` вместо полного обхода (либо кэш summary). Файлы: `services/slots-service/.../service/EarthService.kt`, эндпоинт request-service.
|
||
- [ ] **#53** — (фронт, низкий приоритет/низкая уверенность) `redrawToken` карты на вкладке «Слоты» — сумма длин. В `TguSlotsTab.tsx` токен перерисовки canvas = `filteredSlots.length + bookedSlots.length + selectedKeys.size + (coverageOn ? 1 : 0)`. Разные состояния с одинаковой суммой теоретически не вызовут перерисовку (риск устаревшей картинки на карте). Реалистичную коллизию воспроизвести не удалось (выбор меняет `selectedKeys.size`, след покрытия его отслеживает), поэтому уверенность низкая — занесено для протокола. При желании заменить сумму на устойчивую сигнатуру (напр. хэш ключей видимого набора) или счётчик «версии сцены», инкрементируемый в `scene`-`useMemo`. Файл: `features/tgu-slots/TguSlotsTab.tsx`.
|
||
|
||
### Аутентификация и роли
|
||
- [ ] **#46** — Вывести роли на морду и связать их с аутентификацией. На фронте показывать роль(и) пользователя и разграничивать доступ к действиям/вкладкам по роли; источник ролей — аутентификация (токен/headless-auth, `@nstart/auth`). Уточнить: какие роли есть, где они приходят (claims токена / профиль), как маппятся на UI-разрешения (что скрывать/блокировать), и нужен ли серверный enforcement на стороне сервисов, а не только UI-гейтинг.
|
||
|
||
### Баги
|
||
- [ ] **#8** — Ошибка при попытке принять план на вкладке «Планы».
|
||
- [ ] **#39** — Авторасчёт плана выдаёт нереализуемые маршруты съёмки с креном > 45° (напр. `survey-211`, Emissio 1, крен 46.8°). Такой маршрут нельзя построить вручную: фронт зажимает `MAX_ROUTE_ROLL_DEG = 45` (`TguEditorTab.tsx`), а баллистика/геометрия маршрута и фильтр видимости (МПЛ, `abs(kren) <= 45°`) определены только до 45°. _Диагностика (2026-06-08): корень — данные каталога, не логика. slots-service строит крен слота как центр угловой полосы из каталога (`SlotService.kt:903-905`, `gamma=(begin+end)/2`) и предел 45° не учитывает; полосы берутся напрямую из `satellite_slot_angle`. В сиде каталога (`pcp-satellite-catalog-service/.../V1__initial.sql`) у **оптических** Emissio (id 1-6, 22) собственный профиль полос уходит до 50.42°, и полоса №17 [46.08°, 47.51°] даёт центр ровно 46.8°. Версия «посчитали по логике локации» проверена и НЕ подтвердилась: в slots-service нет ветвления по типу КА (оптика/локация) — все КА строятся одним `TestSatelliteImpl`, солвер выбирается стратегией запроса (GREEDY/CONTINUOUS), `type_code` не используется. Reflexio (локация) наоборот имеет узкие полосы ~17-25°. Решение (вернуться позже): либо обрезать/убрать в каталоге оптические полосы за 45° (поз. №16-20 у Emissio), либо добавить в slots-service зажим/отсев слотов по рабочему пределу крена КА. Истинный предел Emissio подтвердить у владельца каталога/баллистиков перед правкой._
|
||
|
||
### Аудит — надёжность и бэкенд
|
||
_Источник и рекомендации: [аудит-проекта-2026-06-06.md](аудит-проекта-2026-06-06.md)._
|
||
- [ ] **#12** — (B1, высокий) Camunda job-воркеры в `FlowService` молча глотают исключения (`catch { logger.warn(ex.message) }`) при `autoComplete=false`: задача не завершается и не падает, BPMN-процесс зависает без инцидента. Завершать через `newFailCommand`/проброс наверх, логировать `error(msg, ex)`.
|
||
- [ ] **#13** — (B2, средний) Снять симуляционную заглушку `Thread.sleep(5000)` («имитация подтверждения плана») в `FlowService.sendPlan` при появлении реального канала подтверждений.
|
||
- [ ] **#14** — (B3, средний) Привести в порядок несогласованные пакеты-копипасты (ballistics→`pcp_request_service`, complex-mission→`pcp_satellites_service`, ui-service и slots-service делят `slots_service`, request→`org.nstart.dep265`, tgu→без `.pcp.`). Отдельной задачей из-за широкого охвата.
|
||
- [ ] **#15** — (B4, низкий) Опечатка «srpring» в `pcp-srpring-boot-admin-server` растиражирована по каталогу, пакету, CI, docker-compose, gradle. Чинить вместе с #14.
|
||
- [ ] **#16** — (B5, низкий) ~9 мест `logger.warn/error(ex.message)` теряют stack trace — заменить на `logger.error("...", ex)`.
|
||
|
||
### Аудит — фронтенд
|
||
_Источник и рекомендации: [аудит-проекта-2026-06-06.md](аудит-проекта-2026-06-06.md)._
|
||
- [ ] **#17** — (F1, средний) Вынести общий `src/api/http.ts` (`fetchJson` + `extractErrorText` + маппинг ошибок): `extractErrorText` продублирован в 5 клиентах, fetch-обвязка повторяется ~22 раза. Заодно устранить расхождение приоритета полей ошибки (F5).
|
||
- [ ] **#18** — (F2, низкий) Перевести нетипизированный островок `features/constellation/*` (`.jsx`/`.js`, `allowJs`) на TS или добавить декларации типов на границе.
|
||
- [ ] **#19** — (F3, низкий) Удалить мёртвый barrel `features/constellation/index.js` (нигде не импортируется).
|
||
- [ ] **#20** — (F4, низкий) Декомпозировать God-компонент `TguEditorTab.tsx` (1727 строк): вынести хуки (`useDraftHistory`, `useRouteBuilder`), разнести панели.
|
||
|
||
### Аудит — гигиена репозитория и конфиги
|
||
_Источник и рекомендации: [аудит-проекта-2026-06-06.md](аудит-проекта-2026-06-06.md)._
|
||
- [ ] **#21** — (R1, средний) Убрать из git крупные дампы в корне (`pcp_requests_converted_create_requests.json` ~228 КБ, `response_заданияКубернетс.json` ~157 КБ, `response_заданияЕмельянов.json` ~66 КБ) — перенести в `src/test/resources`/`docs` или удалить с добавлением в `.gitignore`.
|
||
- [ ] **#22** — (R2, низкий) Удалить посторонние артефакты в корне: `node_modules/` + пустой `package-lock.json`, `obj_terra.zip`; при желании добавить `/package-lock.json` в `.gitignore`.
|
||
- [ ] **#23** — (R3, низкий) Прогнать `./gradlew clean` / удалить 26 устаревших каталогов `bin/` (задваивают исходники, шумят при grep).
|
||
- [ ] **#24** — (C1, средний) Унифицировать креды БД в config-repo: привести все сервисы к переопределяемому `password: ${DB_PASSWORD:...}` (как в tgu-service), убрать зашитые `password: password`.
|
||
- [ ] **#25** — (C2, низкий) Сузить CORS для прода в `pcp-ui-service` (`allowedOriginPatterns("*")` + `allowCredentials`) до конкретных origin; держать в курсе отсутствие CORS у напрямую вызываемых сервисов (работает только через прокси/nginx).
|
||
|
||
### Аудит — сервис заданий (pcp-request-service)
|
||
_Источник и рекомендации: [аудит-сервис-заданий.md](аудит-сервис-заданий.md) (коды находок RS1–RS7)._
|
||
- [ ] **#30** — (RS2, высокий) Матчинг засчитывает покрытие по любому геометрическому пересечению, игнорируя совместимость аппарата (`survey_type`, крен, разрешение, угол Солнца). Добавить фильтр совместимости маршрут↔заявка перед зачётом покрытия; тип сенсора в паспорте маршрута согласовать в рамках #28. _Отложено до #28 (решение, 2026-06-06): `RoutePassportDto` не несёт явный `surveyType` — контракт фиксируется в рамках аудита связи сервисов #28, тогда же делаем фильтр._
|
||
- [ ] **#33** — (RS5, низкий) Площади считаются планарно в градусах (JTS `area` на EPSG:4326) — искажение для крупных/высокоширотных заявок. Перейти на геодезию (`ST_Area(geography)` или равновеликая проекция), когда вырастут требования к точности. _Отложено по решению аудита: покрытие считается отношением площадей (планар ≈ геодезия для обычных AOI), а переключение `area()` затрагивает matching и точечные ассерты тестов — вводим при росте требований к точности._
|
||
---
|
||
|
||
## Выполнено
|
||
- [x] **Чистки по `/review` `feat/tgu-slots-filters`** _(2026-06-16): по результатам код-ревью ветки применены две безопасные правки. (1) Удалён мёртвый `cancelRequestBooking` в `api/slotsApi.ts` — «Снять всё» перешло на fetch-by-request + батчи `cancelBookedSlots` ради прогресса, вызовов во фронте не осталось (бэкенд-эндпоинт `DELETE /booking/request` цел; совпадения в legacy `pcp-ui-service` не затронуты). (2) `SlotDTO` (`pcp-types-lib`) переведён в `data class`, ручной хелпер `withHolders` (перечисление всех полей) заменён на `slot.copy(holders = …)` в `SlotService.enrichHolders` — убраны грабли «новое поле молча теряется при ручном копировании»; от `SlotDTO` никто не наследуется, 17 мест конструирования совместимы. Kotlin-сборку локально не прогоняли (ограничения реестра @nstart) — проверить при сборке. Остальные находки ревью занесены в #52/#53._
|
||
- [x] **#34** — (RS6) Проброс требуемого процента покрытия из API + разведение терминов. _Готово: в `CreateRequestDto` добавлено опциональное поле `coverageRequiredPercent` (0 < x ≤ 100, по умолчанию 100), оно пробрасывается в сущность и используется как порог COMPLETED; захардкоженная константа убрана. В KDoc/коде разведены «прогресс покрытия» (`coverageRequiredPercent`) и «важность заявки» (`importance`, карта приоритетов ячеек). Тесты: `RequestServiceTest`, `RequestApiDtoContractTest`._
|
||
- [x] **#31** — (RS3) DLQ и error-handler для Kafka-листенера маршрутов. _Готово: `KafkaConsumerConfig` — `DefaultErrorHandler` + `DeadLetterPublishingRecoverer` в топик `pcp.route.georeference-dlq.v1`, экспоненциальный bounded retry, контрактные ошибки (`JsonProcessingException`/`ConstraintViolationException`) → DLQ без ретраев, диагностические заголовки. Настройки `app.kafka.consumer.route.*`, активируется при заданном `spring.kafka.bootstrap-servers`. Тест `KafkaConsumerConfigTest`._
|
||
- [x] **#35** — (RS7) Уведомление **geoportal** о финале заявки. _Готово: единый топик `pcp.request.status.v1` (заменил orphan `pcp.request.completed.v1`) с терминальным статусом в payload (`COMPLETED`/`EXPIRED`). Эмиссия из двух мест через тот же transactional outbox: `RouteMatchingService` (по покрытию) и `RequestExpiryService` (по сроку, #29). Сервис `RequestCompletedOutboxService`→`RequestStatusOutboxService`, eventType `REQUEST_TERMINAL_STATUS`. Outbox получил backoff: колонки `attempts`/`next_attempt_at`, удвоение `initial→max`, предел `max-attempts` (терминальный FAILED). Конфиг `pcp.outbox.*`. Тесты: `OutboxPublisherServiceTest`, `OutboxEventRepositoryJpaTest`, `RequestStatusOutboxServiceTest`, `RequestExpiryServiceTest`, `RouteMatchingServiceTest`. Док: интеракции обновлены. Известный мелкий зазор: заявка, созданная уже просроченной, события не шлёт (вырожденный кейс)._
|
||
- [x] **#29** — (RS1) Планировщик истечения заявок. _Готово: `RequestExpiryService` (`@Scheduled`, `FOR UPDATE SKIP LOCKED`, батчи с верхним лимитом) переводит просроченные `ACCEPTED`/`ACTIVE` в `EXPIRED`; несколько инстансов безопасны. `refreshStatus` теперь вызывается и при создании заявки — статус приёма отражает окно (`ACCEPTED`/`ACTIVE`/`EXPIRED`), а не всегда `ACCEPTED`. Конфиг `pcp.request.expiry.*` в config-repo. Тесты: `RequestExpiryServiceTest` (юнит) + `RequestRepositoryJpaTest` (H2). Разблокирует EXPIRED-эмиссию в #35._
|
||
- [x] **#32** — (RS4) Удалить мёртвый конфиг сервиса заданий. _Готово: из `config-repo/pcp-request-service.yaml` убраны `app.request.cleanup-cron`/`cleanup-retention-days` и неиспользуемый топик `app.kafka.topics.request` (`pcp.request.in.v1`); кода под ними не было, `@Value`-привязок нет._
|
||
- [x] **#26** — Проверить сервис заданий (`pcp-request-service`) по отдельности. _Готово: аудит в [аудит-сервис-заданий.md](аудит-сервис-заданий.md) — карта взаимодействий, учёт покрытия, жизненный цикл, мультиинстанс; находки RS1–RS7 (RS1/RS2 — высокий приоритет)._
|
||
- [x] **#9** — Расширить вправо левую панель информации на вкладке «Редактор» (цифры у границы смотрелись плохо). _Готово: commit 69dfd80, рейл 15.5rem → 17rem._
|
||
- [x] **#10** — Проставлять статусы слотов только после того, как план заложен на КА. _Готово: commit 69dfd80, слоты занимаются по `LAID_IN`, а не по `ACCEPT`._
|
||
- [x] **#11** — Запретить нажимать «Подтвердить закладку», пока начало закладки в будущем. _Готово: commit 69dfd80._
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
Добавил вручную:
|
||
- ~~убрать кнопки + - квадрат с вкладки редактор и карта с карты ибо ничего не делают~~ _Решено иначе (2026-06-11): кнопки не убрали, а починили — они не срабатывали из-за `setPointerCapture` на контейнере карты в `onPointerDown` (перехватывал жест, и клик по кнопке внутри карты не доходил). Добавлен гард: нажатие в области `.tgu-2d-map__zoom` не запускает панорамирование/захват, плюс `stopPropagation` на кнопках. Теперь «+/−» (зум) и «□» (вернуть обзор) работают на всех вкладках с картой._
|
||
- [x] **Линейка на карте** _(2026-06-11): инструмент измерения расстояния на всех вкладках с картой (Карта, Заявки, Комплексный план, Редактор, Слоты — общий `Tgu2DMapView`). Иконка-линейка в правом нижнем углу рядом с «+/−/□», включается/выключается кликом (тумблер). В активном режиме клик по карте ставит вершины замера, рисуется пунктирная ломаная с подписями накопленного расстояния по дуге большого круга (haversine), в бейдже — итоговая длина; «Сброс» очищает замер, повторный клик по иконке выключает. Реализовано целиком в `Tgu2DMapView` (состояние `rulerActive`/`rulerPoints`), стили `.tgu-2d-map__ruler*`._ |