# pcp-tgu-ops-ui — карта возможностей и источников данных

Дата анализа: 2026-05-31.

Документ фиксирует, что сейчас умеет `pcp-tgu-ops-ui`: что отображает, чем управляет, какие backend endpoints вызывает и откуда реально приходят данные. Анализ выполнен по исходному коду frontend и по execution path backend endpoints до service/repository/external source.

## Кратко

`pcp-tgu-ops-ui` — отдельная Vite/React SPA для Mission Ops/TGU. Приложение рендерит `TguPlanningPage` и имеет три вкладки:

- `Планы` — timeline persisted TGU-планов по КА.
- `Карта 2D` — карта с полигонами заявок на съемку.
- `Редактор` — локальный редактор включений КА с черновиком, проверками и заглушкой сохранения.

Реально изменять состояние backend UI умеет только через отправку решения по плану: `ACCEPTED` или `REJECTED`. Редакторские правки пока не сохраняются во внешний сервис.

## Общая схема

```text
Browser
  |
  v
pcp-tgu-ops-ui SPA
  |
  |-- /api/tgu-planning/*
  |     |
  |     v
  |   pcp-ui-service :7008
  |     |
  |     v
  |   pcp-tgu-service :7011
  |     |
  |     |-- pcp_tgu DB
  |     |
  |     `-- Ordinis NSI spacecraft classifier
  |
  |-- /api/current-plans/*
  |     |
  |     v
  |   pcp-ui-service :7008
  |     |
  |     v
  |   pcp-mission-planing-service :7010
  |     |
  |     `-- mission planning DB
  |
  |-- /api/pcp-request/v1/*
  |     |
  |     v
  |   pcp-request-service :7005
  |     |
  |     `-- request DB
  |
  `-- https://*.basemaps.cartocdn.com
        |
        `-- CARTO basemap tiles
```

Dev proxy:

- `/api/pcp-request` -> `http://localhost:7005`, prefix `/api/pcp-request` stripped.
- `/api` -> `http://localhost:7008`.

Production nginx:

- `/api/pcp-request/` -> `http://host.docker.internal:7005/`.
- `/api/` -> `http://host.docker.internal:7008/api/`.

## Endpoint inventory

| UI endpoint | Метод | Использование в UI | Backend execution path | Источник |
|---|---:|---|---|---|
| `/api/tgu-planning/platforms` | GET | список КА, подписи, поиск, редактор | `pcp-ui-service` `TguPlanningRestController.platforms()` -> `TguPlanningService.platforms()` -> `pcp-tgu-service /api/platforms` -> `PlatformService.loadPlatforms()` | in-memory cache, Ordinis NSI |
| `/api/tgu-planning/plans?historyDays=N` | GET | загрузка всех TGU-планов для timeline/map/editor context | `pcp-ui-service` -> `pcp-tgu-service /api/plans` -> `PlanQueryService.getAllPlans()` -> `PlannedPlanRepository.findAllByOrderByStartTime()` | `planned_plan` |
| `/api/tgu-planning/spacecraft/{spacecraftId}/plans?historyDays=N` | GET | есть в API-клиенте, текущая страница в основном грузит все планы | `pcp-ui-service` -> `pcp-tgu-service /api/plans/{spacecraftId}` -> `PlanQueryService.getPlansBySpacecraftId()` | `planned_plan` |
| `/api/tgu-planning/plans/{planId}/decision?decision=...&reason=...` | POST | кнопки принять/отклонить для `WAITING_DECISION` | `pcp-ui-service` -> `pcp-tgu-service /test/tgu/plans/{planId}/decision` -> `TestPlanDecisionService.generateDecision()` -> `HandlePlanDecisionUseCase.handle()` | `planned_plan`, `plan_issue_attempt`, `spacecraft_planning_state`, `processed_plan_decision_event` |
| `/api/pcp-request/v1/requests/map?...` | GET | полигоны заявок на карте 2D и в редакторе | напрямую в `pcp-request-service /v1/requests/map` -> `RequestController.listRequestsForMap()` -> `RequestService.listRequestsForMap()` -> `RequestRepository.findRequests()` | `requests` |
| `/api/current-plans/missions/{missionId}/modes` | GET | контуры существующих включений в редакторе | `pcp-ui-service` -> `pcp-mission-planing-service /api/missions/{missionId}/modes` -> `MissionPlaningService.modesByMission()` | `missions`, `plans`, `modes`, `survey_modes`, `drop_modes`, `drop_mode_survey`, `mode_booking` |

## Вкладка "Планы"

### Что отображает

- Список КА: `name`, `NORAD`, `mission`, `status`.
- Количество планов по КА в выбранном интервале.
- Индикатор проблемных статусов: `REJECTED`, `EXPIRED`, `START_AMBIGUOUS`.
- Timeline persisted TGU-планов по КА.
- Стрелки между последовательными планами одного КА.
- Линию текущего времени, если `now` попадает в выбранный диапазон.
- Детали выбранного плана:
  - `planId`;
  - `spacecraftId`;
  - КА;
  - `startTime`;
  - `endTime`;
  - duration;
  - `kppId`;
  - `status`.

### Чем управляет

- Фильтр диапазона дат.
- Быстрые диапазоны: 24 часа, 3 суток, 7 суток.
- Refresh данных.
- Поиск по КА.
- Выбор конкретного КА.
- Выбор плана.
- Отправка решения `ACCEPTED`/`REJECTED`, но только для плана в статусе `WAITING_DECISION`.

### Execution path решения по плану

```text
User
  |
  v
TguPlanDetails
  |
  |-- Принять -> decision=ACCEPTED, reason=UI_TEST_ACCEPTED
  |
  `-- Отклонить -> decision=REJECTED, reason from prompt
        |
        v
sendPlanDecision()
        |
        v
POST /api/tgu-planning/plans/{planId}/decision
        |
        v
pcp-ui-service
        |
        v
TguPlanningService.sendDecision()
        |
        v
POST pcp-tgu-service /test/tgu/plans/{planId}/decision
        |
        v
TestPlanDecisionService.generateDecision()
        |
        |-- checks plan exists
        |-- checks spacecraft_planning_state exists
        |-- checks activePlanId == planId
        |-- checks activeAttemptId exists and belongs to plan
        |-- checks attempt status STARTING/STARTED
        |
        v
HandlePlanDecisionUseCase.handle()
        |
        |-- insert processed_plan_decision_event, dedup by eventId
        |-- lock plan_issue_attempt by attemptId
        |-- lock planned_plan by planId
        |-- lock/create spacecraft_planning_state
        |
        |-- ACCEPTED:
        |     |-- attempt=ACCEPTED
        |     |-- plan=ACCEPTED
        |     |-- currentBasePlanId=planId
        |     |-- activePlanId/activeAttemptId cleared
        |     |-- enqueue plan_recalculation_job
        |     `-- enqueue accepted_plan_slots_update_job
        |
        `-- REJECTED:
              |-- attempt=REJECTED
              |-- activePlanId/activeAttemptId cleared
              |-- if enough lead time: plan=PLANNED, issueNotBefore=now
              `-- else: plan=EXPIRED, promote next plan or enqueue rebuild
```

Важный caveat: UI отправляет решение через test-only controller `pcp-tgu-service /test/tgu/...`. Если `tgu.test-controller.enabled=false`, эта команда перестанет работать.

## Вкладка "Карта 2D"

### Что отображает

- CARTO dark basemap tiles.
- Lat/lon grid поверх тайлов.
- Полигоны заявок на съемку из `pcp-request-service`.
- Цвета заявок по `surveyType`:
  - `OPTICS`;
  - `RSA`;
  - `COMBINED`.
- Hover tooltip по полигону.
- Выбор полигона заявки на карте.
- Детальную панель с выбранным планом и выбранным объектом карты.

### Чем управляет

- Панорамирование карты drag-ом.
- Zoom колесом и кнопками `+`/`-`.
- Reset view.
- Включение/выключение слоев.
- Загрузка заявок за отдельный интервал.
- Быстрые интервалы заявок: сегодня, 7 дней, 30 дней.
- Скрытие/показ КА в sidebar.

### Что сейчас заявлено в UI, но не имеет данных

В главной карте 2D есть переключатели слоев:

- трасса КА;
- полоса обзора;
- работы плана;
- станции;
- маркеры КА;
- заявки на съемку.

Но текущий `buildTgu2DMapScene()` наполняет только `request` polygons. Для остальных слоев рисовалки существуют, но данные в scene не передаются. UI показывает предупреждение: `Данные слоя недоступны в текущем API.`

### Execution path карты 2D

```text
User
  |
  v
Tgu2DMapTab
  |
  |-- selectedPlan / range / layers / hiddenSpacecraftIds
  |
  |-- load requests
  |     |
  |     v
  |   fetchRequestsForMap()
  |     |
  |     |-- page size = 500
  |     |-- max pages = 10
  |     |-- max loaded requests = 5000
  |     |-- sort = beginDateTime,asc
  |     |
  |     v
  |   GET /api/pcp-request/v1/requests/map
  |     |
  |     v
  |   pcp-request-service
  |     |
  |     v
  |   RequestController.listRequestsForMap()
  |     |
  |     v
  |   RequestService.listRequestsForMap()
  |     |
  |     v
  |   RequestRepository.findRequests()
  |     |
  |     |-- status filter, optional
  |     |-- surveyType filter, optional
  |     |-- kpp filter, supported by backend but not passed by current UI
  |     |-- highPriorityTransmit filter, supported by backend but not passed by current UI
  |     |-- beginFrom/beginTo/endFrom/endTo
  |     |-- exclude DELETED by default
  |     |
  |     v
  |   requests table
  |
  v
buildTgu2DMapScene()
  |
  |-- parse WKT from request.geometry
  |-- supports POLYGON and MULTIPOLYGON
  |-- build MapPolygon objects with layer=requests
  |
  v
Tgu2DMapView
  |
  |-- drawTiles()
  |     |
  |     `-- https://{a|b|c|d}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png
  |
  |-- drawBaseMap()
  |     |
  |     `-- 30 degree lat/lon grid
  |
  |-- drawRequests()
  |     |
  |     `-- request polygons on canvas
  |
  |-- createPolygonSpatialIndex()
  |-- hitTestPolygons()
  |
  v
Canvas + hover tooltip + selected object details
```

### Фильтр заявок на карте

Для выбранного интервала `[from, to]` UI передает:

```text
endFrom = from
beginTo = to
```

То есть выбираются заявки, которые пересекают интервал карты.

## Вкладка "Редактор"

### Что отображает

- Выбранный КА и выбранный TGU-план.
- Черновик включений:
  - `shoot` — съемка;
  - `downlink` — сброс.
- Timeline черновика.
- Окна видимости цели/станции.
- Синтетическую трассу КА на карте редактора.
- Полигоны заявок из `pcp-request-service`.
- Контуры `SURVEY` из `pcp-mission-planing-service`, если endpoint `/current-plans/missions/{id}/modes` вернул режимы.
- Счетчики:
  - всего включений;
  - съемок;
  - сбросов;
  - конфликтов.

### Что умеет делать локально

- Добавить съемку через клик точки на карте.
- Добавить сброс по станции и окну связи.
- Перетаскивать включения по timeline.
- Менять начало/конец включения drag resize.
- Менять время через stepper.
- Дублировать включение.
- Удалять включение.
- Undo/redo.
- Проверять пересечения включений по времени.
- Проверять видимость цели/станции по синтетической орбитальной модели `passes.ts`.
- Показывать контекст других КА в rail, но сейчас `contextLanes.works` пустой.

### Что не умеет сохранять

Кнопка `Применить` вызывает `saveEditedPlan()`, но функция является заглушкой:

```text
Сохранение правок плана во внешний сервис еще не реализовано в backend.
```

Backend endpoint для сохранения отредактированного набора включений отсутствует.

### Execution path редактора

```text
User
  |
  v
TguEditorTab
  |
  |-- selectedSpacecraftId / selectedPlan from TguPlanningPage
  |
  |-- initialize draft
  |     |
  |     v
  |   seedDraft(selectedPlan)
  |     |
  |     |-- if selectedPlan has embedded works:
  |     |     `-- use embedded works
  |     |
  |     `-- else:
  |           `-- create fallback downlink work:
  |                 id = {planId}:plan-window
  |                 kind = downlink
  |                 start/end = selected plan start/end
  |                 stationId = selectedPlan.kppId
  |
  |-- load request context
  |     |
  |     v
  |   fetchRequestsForMap()
  |     |
  |     v
  |   GET /api/pcp-request/v1/requests/map
  |     |
  |     v
  |   pcp-request-service
  |     |
  |     v
  |   requests table
  |
  |-- load plan modes
  |     |
  |     v
  |   fetchPlanModes(selectedPlan.planId)
  |     |
  |     v
  |   GET /api/current-plans/missions/{selectedPlan.planId}/modes
  |     |
  |     v
  |   pcp-ui-service
  |     |
  |     v
  |   pcp-mission-planing-service
  |     |
  |     v
  |   MissionPlaningService.modesByMission()
  |     |
  |     |-- check mission exists
  |     |-- find modes by mission id
  |     |-- map SurveyModeEntity -> SurveyModeResponseDTO
  |     |-- map DropModeEntity -> DropModeResponseDTO
  |     |
  |     v
  |   mission planning DB
  |
  |-- editor map scene
  |     |
  |     |-- groundTrack(spacecraftId) from frontend synthetic orbit
  |     |-- station markers only if station lat/lon exists
  |     |-- plan mode SURVEY contourWkt -> planWorks polygons
  |     `-- request.geometry WKT -> request polygons
  |
  |-- local draft operations
  |     |
  |     |-- addShoot()
  |     |-- addDownlink()
  |     |-- updateWork()
  |     |-- deleteWork()
  |     |-- duplicateWork()
  |     |-- undo()
  |     |-- redo()
  |     |-- overlaps()
  |     `-- checkVisibility()
  |
  `-- apply draft
        |
        v
      saveEditedPlan()
        |
        v
      stub result only, no backend mutation
```

### Важные caveats редактора

- `fetchPlanModes()` использует `selectedPlan.planId` как `missionId` для `/api/current-plans/missions/{missionId}/modes`.
- Если UUID TGU-плана не совпадает с UUID mission в `pcp-mission-planing-service`, endpoint вернет 404, а редактор просто останется без режимов плана.
- Станции строятся из `selectedPlan.kppId`, но координаты станции из backend не загружаются. Поэтому проверка видимости downlink часто возвращает конфликт `Нет координат станции для проверки видимости`.
- Орбитальная модель в редакторе frontend-only и синтетическая: параметры выводятся из hash `spacecraftId`, а не из реальной баллистики/TLE.

## Capability matrix

| Область | Сейчас умеет | Ограничение |
|---|---|---|
| Платформы КА | читать из TGU/NSI, показывать name/NORAD/mission/status | при падении NSI `pcp-tgu-service` может использовать stale cache, если есть |
| TGU-планы | читать persisted plans, строить timeline | UI грузит все планы и фильтрует на клиенте |
| Решение по плану | отправить `ACCEPTED`/`REJECTED` для `WAITING_DECISION` | через test-only controller; нужен active plan + active attempt |
| Карта заявок | загрузить и показать WKT-полигоны заявок | до 5000 заявок, без server-side фильтров kpp/highPriority в UI |
| Слои карты | UI-переключатели есть | кроме заявок, основные слои главной карты не получают данных |
| Редактор включений | локальный draft, drag/resize, add/delete/duplicate, undo/redo, checks | сохранения в backend нет |
| Режимы плана в редакторе | пытается читать `/current-plans/missions/{id}/modes` | возможен mismatch `planId` vs `missionId` |
| Станции | может показывать station markers только при наличии lat/lon | lat/lon станций сейчас не загружаются |
| Тайлы карты | CARTO CDN | внешний network dependency браузера |

## Проверенные исходники

Frontend:

- `services/pcp-tgu-ops-ui/src/App.tsx`
- `services/pcp-tgu-ops-ui/src/api/tguApi.ts`
- `services/pcp-tgu-ops-ui/src/api/requestApi.ts`
- `services/pcp-tgu-ops-ui/src/features/tgu-planning/*`
- `services/pcp-tgu-ops-ui/src/features/tgu-map-2d/*`
- `services/pcp-tgu-ops-ui/src/features/tgu-editor/*`
- `services/pcp-tgu-ops-ui/vite.config.ts`
- `services/pcp-tgu-ops-ui/nginx.conf`

Backend/BFF:

- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TguPlanningController.kt`
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TguPlanningService.kt`
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/CurrentPlansController.kt`
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/MissionService.kt`

TGU:

- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlatformController.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlanController.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlatformService.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryService.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/TestPlanDecisionService.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt`
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/repository/*`

Requests:

- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt`
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt`
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/repository/RequestRepository.kt`
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt`

Mission planning:

- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt`
- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt`
- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/*`
- `libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/*`
