docs: бэклог #49–#53 + дизайн-план выбора слотов полосами
- бэклог-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.
This commit is contained in:
@@ -0,0 +1,177 @@
|
|||||||
|
<!-- /autoplan restore point: /home/dasolovyev/.gstack/projects/missionis-observatio-terrae/feat-tgu-slots-filters-autoplan-restore-20260616-165024.md -->
|
||||||
|
|
||||||
|
# Эргономика выбора слотов: выбор «полосами» + правило выполнимости брони
|
||||||
|
|
||||||
|
## Проблема (как сформулировал оператор)
|
||||||
|
Сделана работа по сортировке, подсветке брони, показу забронированных, расчёту %
|
||||||
|
покрытия. Но **сам выбор слотов под бронь по-прежнему неудобен**: сейчас оператор
|
||||||
|
отмечает слоты галочками поштучно (или Shift+клик диапазоном, или «все видимые /
|
||||||
|
все сутки»). На сотнях-тысячах слотов это утомительно. Плюс выбор «слепой» — он не
|
||||||
|
знает, можно ли вообще физически отснять выбранный набор (манёвренность КА).
|
||||||
|
|
||||||
|
## Решённые premise (premise-гейт, 2026-06-16, скорректировано после CEO-ревью)
|
||||||
|
1. **Ограничение брони через смену крена реально существует** (агилити КА).
|
||||||
|
2. **КОРРЕКЦИЯ:** модель манёвренности УЖЕ есть в slots-service, по-КА из каталога
|
||||||
|
(`mmi`, `maxSurveyDuration`, `maxChainLength`, `ROLL_EPSILON=0.01` — см. ниже).
|
||||||
|
Ручная бронь (`bookReq`) её не проверяет, но coverage-solver и `buildSurveyChains`
|
||||||
|
её соблюдают. Поэтому **не хардкодим физику на фронте** — проверка выполнимости
|
||||||
|
будет **бэкенд validate-эндпоинтом**, переиспользующим эти по-КА параметры.
|
||||||
|
3. **Iteration 1 = только UX выбора полосами** (frontend, без правила). Проверка
|
||||||
|
выполнимости — **iteration 2** (бэкенд validate-эндпоинт + проводка на фронте).
|
||||||
|
|
||||||
|
## Что уже есть в бэкенде (источник правды для it.2)
|
||||||
|
- Параметры по-КА из каталога ([SatelliteCatalogClient.kt:116](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatelliteCatalogClient.kt#L116),
|
||||||
|
[AbstractSatellite.kt:25](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/satellite/AbstractSatellite.kt#L25)):
|
||||||
|
`mmi`=10с (мин. интервал маневра, тот же крен), `maxSurveyDuration`=300с,
|
||||||
|
`maxChainLength`=30 слотов, `ROLL_EPSILON`=0.01.
|
||||||
|
- `buildSurveyChains` / `canMergeInTime` / `sameRoll` ([SlotService.kt:2151](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt#L2151))
|
||||||
|
— склейка цепочек по-КА (для отрисовки), `OccupiedIntervalPrefilter` — mmi/maxSurveyDuration.
|
||||||
|
- **Несоответствия исходного черновика** (закрыты решением «правило за бэком»): фронтовые
|
||||||
|
150с ≠ бэкендовые 300с; фронтовый ε=0.1° ≠ бэкендовый 0.01; «30с на смену крена» —
|
||||||
|
в бэке отдельной такой константы НЕТ (склейка только при том же крене). Эти числа
|
||||||
|
сверяются с каталогом/баллистиками в it.2, НЕ хардкодятся.
|
||||||
|
|
||||||
|
## Что уже есть (карта на существующий код)
|
||||||
|
- Выбор: `selectedKeys: Set<slotKey>` в [TguSlotsTab.tsx](../../services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx).
|
||||||
|
Поштучный чек, Shift+клик-диапазон ([slotSelection.ts](../../services/pcp-tgu-ui-service/src/features/tgu-slots/slotSelection.ts) `selectionAfterCheck`),
|
||||||
|
«выбрать видимые», «все сутки», «снять выбор».
|
||||||
|
- Слот несёт `roll`, `revolution`, `revolutionSign`, `cycle`, `satelliteId`, `slotNumber`, `tn`, `tk`, `state`.
|
||||||
|
- Чистые модули с юнит-тестами: slotFilters.ts, slotSelection.ts, coverage.ts (+ .test.ts) — образец стиля.
|
||||||
|
- Бронь: `book()` → подтверждение → `performBook` (чанк 200), снятие аналогично.
|
||||||
|
- Бэкенд бронирует без валидации манёвренности ([SlotService.kt:2500](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt#L2500)). В it.1 не трогаем.
|
||||||
|
|
||||||
|
## Решение — Iteration 1 (только UX выбора полосами, frontend) — ФИНАЛ после ревью
|
||||||
|
|
||||||
|
### Модуль 1: `slotStrips.ts` (чистая логика + юнит-тесты)
|
||||||
|
- `groupStrips(slots, keyOf): Map<slotKey, StripId>` — максимальные same-roll цепочки
|
||||||
|
**по-КА**. `keyOf` — аксессор ключа слота (как `keyAt` в `selectionAfterCheck`), чтобы
|
||||||
|
не дублировать формулу `${satelliteId}:${slotNumber}:${cycle}`.
|
||||||
|
- **Детерминизм:** внутри функции тотальный порядок — сортировка по
|
||||||
|
`(satelliteId, parseLocal(tn), parseLocal(tk), slotNumber, cycle, slotKey)`. Результат
|
||||||
|
не зависит от порядка входного массива.
|
||||||
|
- **ε крена = `ROLL_EPSILON = 0.01`** (именованная константа, как в бэкенде; НЕ 0.1).
|
||||||
|
Сравнение — **с якорным креном полосы** (`abs(roll − anchorRoll) ≤ 0.01`), НЕ с
|
||||||
|
предыдущим слотом (иначе дрейф крена 0.00→0.009→0.018 ошибочно склеит). Совпадает с
|
||||||
|
бэкендовым `sameRoll(roll, chainRoll)`.
|
||||||
|
- **StripId стабилен и читаем:** `strip:${slotKey первого члена}` (не счётчик — счётчик
|
||||||
|
зависит от порядка обхода КА).
|
||||||
|
- Краевые: один слот → своя полоса; два КА вперемешку по времени НЕ склеиваются;
|
||||||
|
`tn` не парсится (NaN) → синглтон без падения; ровно на границе ε (`==0.01`) → та же полоса.
|
||||||
|
|
||||||
|
### Модуль 2: выбор «полосами» (UX в TguSlotsTab) — решения ревью
|
||||||
|
- **Жест: чип в строке `полоса ×N`** (N = число видимых членов полосы). НЕ Alt+клик
|
||||||
|
(необнаружим, конфликт с чек/Shift), НЕ клик по карте как основной. Чип появляется
|
||||||
|
только при N≥2 и если есть хоть один выбираемый (не-BUSY) член.
|
||||||
|
- **Identity ≠ selection target:**
|
||||||
|
- `stripIdByKey = useMemo(groupStrips(displaySlots, slotKey), [displaySlots])` — id по
|
||||||
|
СТАБИЛЬНОМУ набору (не filteredSlots/sortedSlots — иначе фильтр-дырка ложно склеит/разорвёт).
|
||||||
|
- Видимые выбираемые члены полосы берём из `filteredSlots` ∩ `!slotIsBusy` (по образцу
|
||||||
|
`selectableVisible`).
|
||||||
|
- **Поведение чипа = `toggleAllVisible`, но в рамках одной полосы:** если ВСЕ видимые
|
||||||
|
выбираемые члены уже выбраны → снять их; иначе добавить. После — `anchorIndexRef.current = null`.
|
||||||
|
Без нового state (`selectedStrips` не заводим — рассинхрон с `selectedKeys`).
|
||||||
|
- **Группировка визуально по StripId, sort-aware:** в сортировке «время» — левый рельс
|
||||||
|
(border-left, цвет по `StripId`, приглушённый) на смежных строках полосы. В «крен»/«вклад»
|
||||||
|
слоты полосы НЕ подряд → рельс НЕ рисуем; подсветка членов полосы по StripId при наведении.
|
||||||
|
Фон строки оставляем за состоянием (active/selected); статус-ячейки не перекрашиваем.
|
||||||
|
- **Карта it.1:** оставляем существующую подсветку выбранных слотов (`markedObjectIds` уже
|
||||||
|
ведётся от `selectedKeys`). Отдельную раскраску полос на карте и «выбрать полосу из
|
||||||
|
`SlotPickMenu`» — НЕ делаем в it.1 (расширяет scope на модель/рисовалку карты).
|
||||||
|
- **CSS/таблица:** обновить `colCount`; выравнивание ячеек — классами, не сдвигом
|
||||||
|
`nth-child`; чип держать в пределах `ROW_HEIGHT = 34`.
|
||||||
|
|
||||||
|
### Матрица состояний строки (зафиксировать перед реализацией)
|
||||||
|
| Состояние | Источник | Визуал |
|
||||||
|
|---|---|---|
|
||||||
|
| active (фокус по карте) | `activeSlotId` | подсветка строки (как есть) |
|
||||||
|
| selected | `selectedKeys.has(key)` | чек отмечен |
|
||||||
|
| strip-selected | все видимые выбираемые члены полосы выбраны | чип в активном виде |
|
||||||
|
| partial | часть членов полосы выбрана | чип в нейтральном виде |
|
||||||
|
| disabled chip | N<2 или 0 выбираемых членов | чипа нет |
|
||||||
|
| booked/occupied | как есть (бейдж) | не меняется; в полосу входит визуально, в выбор — нет |
|
||||||
|
|
||||||
|
## Реализовано (it.1, 2026-06-17)
|
||||||
|
- `slotStrips.ts` (`groupStrips`, `ROLL_EPSILON=0.01`, якорный крен, тотальный порядок) +
|
||||||
|
`slotStrips.test.ts` (11 кейсов: мультиполоса, два КА, граница ε, дрейф крена, порядок,
|
||||||
|
tie-break, NaN `tn`, пустой вход).
|
||||||
|
- `toggleStripSelection` в `slotSelection.ts` (чистый, без мутации) + тесты.
|
||||||
|
- `TguSlotsTab.tsx`: `stripIdByKey` (memo над `displaySlots`), `stripInfo` (видимые члены /
|
||||||
|
выбираемые не-BUSY по полосе), чип `полоса ×N` в ячейке «КА» (активен при всех выбранных),
|
||||||
|
рельс по `StripId` (sortMode `time`), подсветка членов по `StripId` при наведении.
|
||||||
|
- Смежный баг закрыт: `anchorIndexRef` сбрасывается при смене видимого набора (`filteredSlots`).
|
||||||
|
- Тесты 123 зелёных, `tsc -b` чисто, `vite build` ок. Бэкенд/DTO не тронуты.
|
||||||
|
|
||||||
|
## Решение — Iteration 2 (проверка выполнимости, отдельно)
|
||||||
|
- Бэкенд: read-only `POST /api/slots/booking/validate` (набор слотов → violations),
|
||||||
|
переиспользует по-КА `mmi`/`maxSurveyDuration`/`maxChainLength`/`ROLL_EPSILON` из каталога.
|
||||||
|
Сверить точную семантику (300 vs «150 на полосу», нужна ли отдельная 30с-граница) с
|
||||||
|
баллистиками. Опционально позже — валидация и в самом `bookReq`.
|
||||||
|
- Фронт: гонять validate при выборе/перед бронью, показывать violations, решить
|
||||||
|
блокировать кнопку или предупреждать.
|
||||||
|
|
||||||
|
## NOT in scope (it.1)
|
||||||
|
- Любая физика выполнимости на фронте (перенесено в it.2, за бэкенд).
|
||||||
|
- Переработка автоподбора покрытия (GREEDY/CONTINUOUS).
|
||||||
|
- Изменение модели хранения броней.
|
||||||
|
|
||||||
|
## Тест-план (it.1) — финал
|
||||||
|
- `slotStrips.test.ts` (groupStrips): мультиполоса в одном КА; два КА вперемешку по времени
|
||||||
|
(не склеиваются); один слот → своя полоса; граница ε (`diff==0.01` склеивает, `0.0101` рвёт);
|
||||||
|
дрейф крена 0.00/0.009/0.018 (с якорным сравнением → две полосы); устойчивость к порядку
|
||||||
|
входа (равенство РАЗБИЕНИЯ при перемешивании); tie-break по равному `tn`; NaN `tn` → синглтон
|
||||||
|
без падения; пустой вход → пустая Map.
|
||||||
|
- **Чистый хелпер выбора полосы** (выделить из компонента, как slotSelection): скрытые члены
|
||||||
|
не выбираются; BUSY исключён; «все видимые выбраны → снять, иначе добавить»; нет мутации
|
||||||
|
входного `selectedKeys`.
|
||||||
|
- Ручная проверка в vite dev: чип `полоса ×N`, рельс в сортировке «время», подсветка по
|
||||||
|
StripId в «крен»/«вклад», согласованность с Shift+клик/чек/«все сутки», счётчик «Выбрано».
|
||||||
|
|
||||||
|
## Смежная находка (вне scope, флаг оператору)
|
||||||
|
`anchorIndexRef` (якорь Shift-выбора) сбрасывается при смене сортировки, но НЕ при смене
|
||||||
|
фильтров (статус/КА/ветвь/сутки), хотя они тоже меняют порядок строк → Shift-диапазон после
|
||||||
|
фильтрации может выбрать не то. Существующий латентный баг, рядом с этой задачей. Дёшево
|
||||||
|
починить (сбрасывать якорь и при смене фильтров) — включить в it.1 или отдельным фиксом.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Review artifacts (/autoplan, 2026-06-16)
|
||||||
|
|
||||||
|
### CEO consensus
|
||||||
|
| # | Вопрос | Claude | Codex | Консенсус |
|
||||||
|
|---|--------|--------|-------|-----------|
|
||||||
|
| 1 | Та ли проблема | да (UX выбора) | да | CONFIRMED |
|
||||||
|
| 2 | Фронт-физика — регрет? | да, критично | да, критично | CONFIRMED → правило за бэк |
|
||||||
|
| 3 | Бэкенд уже имеет модель? | да (нашёл chain-логику) | — | подтверждено в коде |
|
||||||
|
| 4 | Калибровка scope | разбить | разбить | CONFIRMED → it.1 только UX |
|
||||||
|
|
||||||
|
### Design consensus
|
||||||
|
| Тема | Claude | Codex | Консенсус |
|
||||||
|
|------|--------|-------|-----------|
|
||||||
|
| Жест | чип в строке | чип в строке | CONFIRMED |
|
||||||
|
| Grouping vs sort | sort-aware по StripId | sort-aware по StripId | CONFIRMED |
|
||||||
|
| Скрытые члены | только видимые | только видимые | CONFIRMED |
|
||||||
|
| Карта | вторично/отложить | вторично/отложить | CONFIRMED |
|
||||||
|
| Состояния | матрица | матрица | CONFIRMED |
|
||||||
|
|
||||||
|
### Eng consensus
|
||||||
|
| Тема | Claude | Codex | Консенсус |
|
||||||
|
|------|--------|-------|-----------|
|
||||||
|
| StripId над displaySlots | да | да | CONFIRMED |
|
||||||
|
| Identity ≠ selection target | да | да | CONFIRMED |
|
||||||
|
| Memo на [displaySlots] | да | да | CONFIRMED |
|
||||||
|
| ε: якорь vs предыдущий | оставил открытым | якорь (анти-дрейф) | DISAGREE → решено: якорь |
|
||||||
|
| Бэкенд-изменения в it.1 | нет | нет | CONFIRMED |
|
||||||
|
| Сброс якоря на фильтрах | — | да (смежный баг) | флаг оператору |
|
||||||
|
|
||||||
|
### Decision Audit Trail
|
||||||
|
| # | Фаза | Решение | Класс | Принцип | Обоснование |
|
||||||
|
|---|------|---------|-------|---------|-------------|
|
||||||
|
| 1 | premise | Правило выполнимости — за бэкенд (не фронт-хардкод) | User Challenge | — | оба ревьюера + бэкенд уже имеет по-КА модель из каталога |
|
||||||
|
| 2 | premise | it.1 = только UX выбора; правило → it.2 | User Challenge | P2/P3 | развязать безопасный win от спорной физики |
|
||||||
|
| 3 | design | Жест = чип `полоса ×N` | Taste | P5 (явное) | обнаружим, не конфликтует с чек/Shift |
|
||||||
|
| 4 | design | Grouping sort-aware (рельс в «время», подсветка иначе) | Mechanical | P1 | зебра по строкам врёт вне time-сортировки |
|
||||||
|
| 5 | design | Чип выбирает только видимые не-BUSY члены | Mechanical | P1 | не выбирать невидимое (доверие счётчику) |
|
||||||
|
| 6 | eng | StripId над `displaySlots`, memo на `[displaySlots]` | Mechanical | P3 | стабильность id + перф на тысячах строк |
|
||||||
|
| 7 | eng | ε-сравнение с якорным креном полосы | Mechanical | P1 | анти-дрейф, совпадает с бэкендовым sameRoll |
|
||||||
|
| 8 | eng | Без бэкенд/DTO-изменений в it.1 | Mechanical | P4 | всё нужное уже в SlotDTO |
|
||||||
|
| 9 | eng | Карта — без раскраски полос в it.1 | Mechanical | P3/P5 | не расширять scope на модель карты |
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
> Задачи #12–#25 заведены по результатам аудита. Подробности, обоснование и рекомендации
|
> Задачи #12–#25 заведены по результатам аудита. Подробности, обоснование и рекомендации
|
||||||
> по каждой — в [аудит-проекта-2026-06-06.md](аудит-проекта-2026-06-06.md) (ссылки на коды находок B/F/R/C).
|
> по каждой — в [аудит-проекта-2026-06-06.md](аудит-проекта-2026-06-06.md) (ссылки на коды находок B/F/R/C).
|
||||||
|
|
||||||
_Обновлено: 2026-06-15_
|
_Обновлено: 2026-06-16_
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ _Обновлено: 2026-06-15_
|
|||||||
- **Главный риск — Kafka:** `KafkaConsumerConfig`/сериализаторы завязаны на `com.fasterxml` ObjectMapper. Перед стартом — спайк (~полдня): умеет ли `spring-kafka` в нашем Boot 4 Jackson 3-сериализаторы. Если нет — Kafka-слой мигрируем отдельно, HTTP/внутренний JSON переводим независимо.
|
- **Главный риск — 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 закрывает причину бага, полную миграцию вести фоново сервис за сервисом.
|
- **Масштаб (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 и базовый образ контейнера.
|
- [ ] **#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» точки трассы с другим номером выпадают и пропадает орбитальный контекст. Свести нумерацию витков к единому источнику (или маппинг на границе), чтобы трасса и маршрут согласовывались по витку.
|
- [ ] **#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» точки трассы с другим номером выпадают и пропадает орбитальный контекст. Свести нумерацию витков к единому источнику (или маппинг на границе), чтобы трасса и маршрут согласовывались по витку.
|
||||||
|
|
||||||
### Проверка сервисов
|
### Проверка сервисов
|
||||||
@@ -65,6 +66,17 @@ _Обновлено: 2026-06-15_
|
|||||||
- [ ] **#43** — Уточнение статуса слота. На вкладке «Слоты» уже показывается базовый статус слота (`state`: свободен/бронь/занят) и бейдж брони по заявке. Осталось: подтягивать/обновлять актуальный статус занятости по **заложенному** плану (слоты занимаются по `LAID_IN`, а не по `ACCEPT`, ср. #10) и согласовать его источник со slots-service (real-time/после закладки), а также прокинуть статус на вкладки «Карта»/«Редактор» (#3).
|
- [ ] **#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, что соответствуют выбранному слоту, а не пересчитывает их в базовом фрейме.
|
- [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-гейтинг.
|
- [ ] **#46** — Вывести роли на морду и связать их с аутентификацией. На фронте показывать роль(и) пользователя и разграничивать доступ к действиям/вкладкам по роли; источник ролей — аутентификация (токен/headless-auth, `@nstart/auth`). Уточнить: какие роли есть, где они приходят (claims токена / профиль), как маппятся на UI-разрешения (что скрывать/блокировать), и нужен ли серверный enforcement на стороне сервисов, а не только UI-гейтинг.
|
||||||
|
|
||||||
@@ -102,6 +114,7 @@ _Источник и рекомендации: [аудит-сервис-зада
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Выполнено
|
## Выполнено
|
||||||
|
- [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] **#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] **#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] **#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`. Док: интеракции обновлены. Известный мелкий зазор: заявка, созданная уже просроченной, события не шлёт (вырожденный кейс)._
|
||||||
|
|||||||
Reference in New Issue
Block a user