docs: удалить неактуальные TASK-доки (datetime-utc, tgu-editor-integration, plan-on-demand-redesign)
This commit is contained in:
@@ -1,198 +0,0 @@
|
|||||||
# Задача: единообразие времени (UTC) по всей группе сервисов `pcp`
|
|
||||||
|
|
||||||
> Документ-ТЗ для кодинг-агента. Раздел 0 — промпт для запуска (скопировать в агента).
|
|
||||||
> Разделы 1–9 — инструкция, которую агент читает в репозитории и которой следует.
|
|
||||||
> Стиль работы, границы слоёв и формат финального ответа — по корневому `AGENTS.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Промпт для запуска агента (copy-paste)
|
|
||||||
|
|
||||||
```
|
|
||||||
Ты работаешь в монорепозитории observatio-terrae (pcp).
|
|
||||||
|
|
||||||
Задача: провести аудит и привести к единообразию работу со временем (датами) по всей
|
|
||||||
группе сервисов pcp. Целевое соглашение: ВСЁ ВРЕМЯ — UTC, представление на API не зависит
|
|
||||||
от часового пояса машины-клиента или сервера.
|
|
||||||
|
|
||||||
Перед началом обязательно прочитай:
|
|
||||||
1. /AGENTS.md — правила работы, стиль, формат финального ответа.
|
|
||||||
2. /docs/tasks/TASK_datetime-utc-consistency.md — это ТЗ (данный файл).
|
|
||||||
3. Эталонную реализацию на фронте: services/pcp-tgu-ops-ui/src/utils/utcTime.ts
|
|
||||||
и историю правок в pcp-tgu-ops-ui (вкладки «Планы», «Редактор», «Заявки», «Карта 2D»).
|
|
||||||
|
|
||||||
Это в первую очередь АУДИТ + точечные правки, а не массовый рефактор. Сначала инвентаризация
|
|
||||||
(раздел 4), затем фиксы по чек-листу (раздел 5) с приоритетом на места, где сдвиг времени
|
|
||||||
виден пользователю или уходит на бэкенд при записи. Backend (Kotlin) трогать только там, где
|
|
||||||
сериализация реально неоднозначна (раздел 6) — сериализационные правки сопровождать тестом.
|
|
||||||
|
|
||||||
Не ломай чужие контракты: если меняешь формат на проводе — обнови потребителей и тесты.
|
|
||||||
После каждого модуля прогоняй его тесты/сборку. В конце дай отчёт по формату из AGENTS.md
|
|
||||||
плюс заполненную таблицу инвентаризации (раздел 4) и список расхождений.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Контекст и зачем это
|
|
||||||
|
|
||||||
Время в системе хранится и передаётся как **UTC**, но представление на границах API
|
|
||||||
неоднородно: часть сервисов отдаёт naive `LocalDateTime` (ISO без зоны, напр.
|
|
||||||
`"2026-05-29T01:00:00"`, по соглашению — UTC), часть — `OffsetDateTime`/`Instant`
|
|
||||||
(ISO с зоной, напр. `"2026-05-29T01:00:00Z"`). Эта разница уже привела к багу во фронте:
|
|
||||||
|
|
||||||
- `pcp-tgu-service` отдаёт планы как naive `LocalDateTime`.
|
|
||||||
- Фронт парсил такие строки через `new Date(s)` / `Date.parse(s)`, а JS трактует строку
|
|
||||||
**без зоны как локальное время браузера** → сдвиг на офсет (для МСК — +3 ч).
|
|
||||||
- Одни вкладки показывали через `toLocaleString` (пояс браузера), другие — в UTC →
|
|
||||||
одно и то же время выглядело по-разному на разных экранах и у разных операторов.
|
|
||||||
- В пути **записи** заявки оператор, вводя `01:00`, отправлял на бэк `22:00`.
|
|
||||||
|
|
||||||
Корень — не «UTC против местного», а **несогласованность парсинга/форматирования** и
|
|
||||||
**неоднородность формата на проводе между сервисами**. Эта задача делает всё единообразным.
|
|
||||||
|
|
||||||
## 2. Целевое соглашение (canonical)
|
|
||||||
|
|
||||||
1. **Всё время — UTC.** На любом API времена представляют момент в UTC.
|
|
||||||
2. **Формат на проводе — ISO-8601 с явной зоной `Z`** (предпочтительно). Где сервис
|
|
||||||
исторически отдаёт naive `LocalDateTime`, это допустимо **только** при железном правиле
|
|
||||||
«naive = UTC», но такой эндпоинт — кандидат на миграцию к `Z` (раздел 6).
|
|
||||||
3. **Фронт:**
|
|
||||||
- парсинг строк бэка — только через `src/utils/utcTime.ts` (`parseUtc`); никаких
|
|
||||||
«голых» `new Date(string)` / `Date.parse(string)`;
|
|
||||||
- отображение — только UTC (`formatUtcDateTime/Date/Time`, либо `toLocale*` с
|
|
||||||
`timeZone: "UTC"`); никакого `toLocaleString` без `timeZone`;
|
|
||||||
- `<input type="datetime-local">` — значение через `toUtcInputValue`, чтение обратно
|
|
||||||
через `parseUtc`; отправка на offset-бэк — через `toUtcIso`.
|
|
||||||
4. **Никакой неявной локализации** под пояс машины ни на фронте, ни в логах/ответах бэка.
|
|
||||||
|
|
||||||
## 3. Эталон (уже сделано — переиспользовать, не переделывать)
|
|
||||||
|
|
||||||
В `pcp-tgu-ops-ui` это соглашение уже внедрено и может служить образцом:
|
|
||||||
|
|
||||||
- `src/utils/utcTime.ts` — `parseUtc`, `formatUtcDateTime/Date/Time`, `toUtcInputValue`,
|
|
||||||
`toUtcIso`. **Единая точка** парсинга/форматирования.
|
|
||||||
- Переведены вкладки «Планы», «Редактор», «Заявки», «Карта 2D» (источник `startMs/endMs`
|
|
||||||
в `tguTimelineMapper`, пикеры диапазона, детали заявок, путь создания заявки).
|
|
||||||
- Нюанс: `pcp-request-service` отдаёт `OffsetDateTime` (строки с `Z`) — `parseUtc` их тоже
|
|
||||||
корректно обрабатывает (зона есть → не добавляет `Z`), а запись идёт через `toUtcIso`.
|
|
||||||
|
|
||||||
Любой новый код во фронте обязан следовать этому слою.
|
|
||||||
|
|
||||||
## 4. Инвентаризация (заполнить в отчёте)
|
|
||||||
|
|
||||||
Стартовый срез (тип `LocalDateTime` vs `OffsetDateTime/ZonedDateTime/Instant` в файлах
|
|
||||||
`dto|controller|api|response|model`); агент должен уточнить и довести до пер-эндпоинтной
|
|
||||||
картины «что именно отдаётся наружу»:
|
|
||||||
|
|
||||||
| Сервис | Преобладает на границе API | Действие |
|
|
||||||
|---|---|---|
|
|
||||||
| pcp-tgu-service | naive `LocalDateTime` | задокументировать «=UTC», кандидат на `Z` |
|
|
||||||
| pcp-request-service | `OffsetDateTime` (`Z`) | эталон, проверить полноту |
|
|
||||||
| pcp-dynamic-plan-service | naive `LocalDateTime` (+1 offset-файл) | проверить смешение |
|
|
||||||
| pcp-complex-mission-service | naive `LocalDateTime` (+2 offset) | проверить смешение |
|
|
||||||
| pcp-ui-service | naive `LocalDateTime` (+1 offset) | проверить (агрегатор/BFF?) |
|
|
||||||
| slots-service | naive `LocalDateTime` | задокументировать «=UTC» |
|
|
||||||
| pcp-coverage-scheme-service | naive `LocalDateTime` | задокументировать «=UTC» |
|
|
||||||
| pcp-ballistics-service | naive `LocalDateTime` | задокументировать «=UTC» |
|
|
||||||
| pcp-mission-planing-service | naive `LocalDateTime` | задокументировать «=UTC» |
|
|
||||||
| pcp-route-processing-service | нет времени на границе (проверить) | — |
|
|
||||||
| pcp-satellite-catalog-service | нет (проверить) | — |
|
|
||||||
| pcp-stations-service | нет (проверить) | — |
|
|
||||||
| tle-monitoring-service | нет (проверить) | — |
|
|
||||||
|
|
||||||
Колонки итоговой таблицы: **сервис · эндпоинт/поле · тип · формат на проводе (пример) ·
|
|
||||||
naive? · потребители · вердикт (ok / задокументировать / мигрировать на `Z`)**.
|
|
||||||
|
|
||||||
Полезно: во всех проверенных сервисах Jackson настроен с `JavaTimeModule` и
|
|
||||||
`disable(WRITE_DATES_AS_TIMESTAMPS)` → времена сериализуются как ISO-строки (не числа),
|
|
||||||
поэтому `LocalDateTime` уходит без зоны, `OffsetDateTime`/`Instant` — с `Z`/офсетом.
|
|
||||||
|
|
||||||
## 5. Фронт: инвариант и фиксы (`pcp-tgu-ops-ui`)
|
|
||||||
|
|
||||||
Эталон внедрён, но нужно **закрепить как инвариант** и проверить, что ничего не утекло:
|
|
||||||
|
|
||||||
1. Найти нарушителей (раздел 7, паттерн A). Любой «голый» разбор/формат строки времени
|
|
||||||
бэка чинить через `utils/utcTime`.
|
|
||||||
2. Добавить **защиту от регрессий** — на выбор:
|
|
||||||
- юнит-тест-«страж», который грепает `src/` и падает на запрещённых паттернах
|
|
||||||
(`new Date(<string>)`, `Date.parse(`, `toLocaleString(` без `timeZone`,
|
|
||||||
`formatDateTimeLocal`); **или**
|
|
||||||
- ESLint-правило (`no-restricted-syntax`) с тем же смыслом.
|
|
||||||
3. Прогнать `npm run test` и `npm run build` в модуле.
|
|
||||||
|
|
||||||
## 6. Backend: когда и что править
|
|
||||||
|
|
||||||
По умолчанию — **не менять формат на проводе** (это контракт). Действия:
|
|
||||||
|
|
||||||
1. **Документировать инвариант.** Для сервисов с naive `LocalDateTime`: явно зафиксировать
|
|
||||||
«все времена на API — UTC» (KDoc у DTO + запись в `docs/standards`, раздел 8). Это дёшево
|
|
||||||
и безопасно, и снимает неоднозначность для будущих клиентов.
|
|
||||||
2. **Чинить реальные баги.** Если найдётся место, где сервис:
|
|
||||||
- кладёт в naive-поле **локальное** время сервера (`LocalDateTime.now()` вместо
|
|
||||||
`LocalDateTime.now(ZoneOffset.UTC)` / `Instant.now()`), или
|
|
||||||
- конвертирует под зону JVM при чтении/записи —
|
|
||||||
это **баг**: исправить на UTC, добавить тест.
|
|
||||||
3. **Миграция к явному `Z` (опционально, по согласованию).** Перевод naive `LocalDateTime`
|
|
||||||
на `OffsetDateTime`/`Instant` (UTC) на границе — самый чистый итог, но это смена контракта:
|
|
||||||
делать только с обновлением потребителей (на фронте `parseUtc` уже совместим с обоими) и
|
|
||||||
сериализационным тестом. Оформить как отдельные under-задачи per-service, не «большим взрывом».
|
|
||||||
4. **Тест формата.** Для каждого сервиса, отдающего время, иметь тест, фиксирующий формат на
|
|
||||||
проводе (есть `Z` / нет зоны) — чтобы контракт не «уехал» молча.
|
|
||||||
|
|
||||||
## 7. Как искать нарушителей (детект-паттерны)
|
|
||||||
|
|
||||||
**A. Фронт (`services/pcp-tgu-ops-ui/src`), запрещено вне `utils/utcTime.ts`:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# разбор строки как локального времени
|
|
||||||
grep -rn "new Date([a-zA-Z].*)\.getTime\|Date\.parse(" src --include=*.ts --include=*.tsx | grep -v utcTime
|
|
||||||
# показ в поясе браузера
|
|
||||||
grep -rn "toLocaleString\|toLocaleDateString\|toLocaleTimeString" src --include=*.ts --include=*.tsx | grep -v 'timeZone: "UTC"'
|
|
||||||
# локальные хелперы пикеров
|
|
||||||
grep -rn "formatDateTimeLocal" src
|
|
||||||
# локальные компоненты даты
|
|
||||||
grep -rn "getFullYear()\|getMonth()\|getDate()\|getHours()\|getMinutes()" src --include=*.ts --include=*.tsx | grep -v getUTC
|
|
||||||
```
|
|
||||||
|
|
||||||
**B. Backend (Kotlin) — потенциально локальное «сейчас» в UTC-полях:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# now() без UTC-зоны
|
|
||||||
grep -rn "LocalDateTime\.now()\|LocalDate\.now()\|LocalTime\.now()" services/*/src/main
|
|
||||||
# конвертации под зону системы
|
|
||||||
grep -rn "atZone(\|ZoneId\.systemDefault\|toLocalDateTime()\|systemDefault()" services/*/src/main
|
|
||||||
# типы времени на границе (для инвентаризации)
|
|
||||||
grep -rn "LocalDateTime\|OffsetDateTime\|ZonedDateTime\|Instant" services/*/src/main | grep -iE "dto|controller|response"
|
|
||||||
```
|
|
||||||
|
|
||||||
Каждое попадание классифицировать: **ок / задокументировать / починить / мигрировать**.
|
|
||||||
|
|
||||||
## 8. Зафиксировать стандарт
|
|
||||||
|
|
||||||
Создать `docs/standards/DATETIME_UTC.md` (на него ссылаться из KDoc/комментариев):
|
|
||||||
|
|
||||||
- соглашение из раздела 2 (всё UTC; формат на проводе; правила фронта);
|
|
||||||
- таблица состояния сервисов (итог раздела 4);
|
|
||||||
- правило для нового кода: фронт — только `utils/utcTime`; бэк — UTC-`now`,
|
|
||||||
предпочтительно `Z` на новых API.
|
|
||||||
|
|
||||||
При наличии общего чек-листа в `docs/standards/CHECKLIST.md` — добавить пункт
|
|
||||||
«Время на API — UTC; фронт парсит/форматирует через utcTime».
|
|
||||||
|
|
||||||
## 9. Definition of Done
|
|
||||||
|
|
||||||
- [ ] Заполнена пер-эндпоинтная таблица инвентаризации (раздел 4) по всем 13 сервисам.
|
|
||||||
- [ ] Фронт: нарушителей по паттернам раздела 7A не осталось; добавлена защита от регрессий
|
|
||||||
(тест-страж или ESLint-правило); `npm run test` и `npm run build` зелёные.
|
|
||||||
- [ ] Backend: задокументирован инвариант «API = UTC» у сервисов с naive `LocalDateTime`;
|
|
||||||
найденные реальные баги локального `now`/конвертаций исправлены с тестами.
|
|
||||||
- [ ] Создан `docs/standards/DATETIME_UTC.md`; обновлён `CHECKLIST.md` (если есть).
|
|
||||||
- [ ] Список расхождений и предложенные under-задачи на миграцию к `Z` (если такие нужны)
|
|
||||||
приведены в финальном отчёте (формат — `AGENTS.md`).
|
|
||||||
|
|
||||||
### Не делать
|
|
||||||
|
|
||||||
- Массовую смену формата на проводе без обновления потребителей и тестов.
|
|
||||||
- Изменение семантики хранения в БД (там уже UTC) — только представление/парсинг.
|
|
||||||
- Локализацию под пояс машины «чтобы было привычнее» — это и есть источник проблемы.
|
|
||||||
```
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
# Задача: встроить интерфейс «Редактор включений КА» в `pcp-tgu-ops-ui`
|
|
||||||
|
|
||||||
> Документ-ТЗ для кодинг-агента. Раздел 0 — промпт для запуска (скопировать в агента).
|
|
||||||
> Разделы 1–11 — инструкция, которую агент читает в репозитории и которой следует.
|
|
||||||
> Стиль работы, границы слоёв и формат финального ответа — по корневому `AGENTS.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Промпт для запуска агента (copy-paste)
|
|
||||||
|
|
||||||
```
|
|
||||||
Ты работаешь в монорепозитории observatio-terrae (pcp).
|
|
||||||
|
|
||||||
Задача: встроить прототип интерфейса «Редактор включений КА» в существующее
|
|
||||||
фронтенд-приложение services/pcp-tgu-ops-ui (Vite + React 19 + TypeScript).
|
|
||||||
|
|
||||||
Перед началом обязательно прочитай:
|
|
||||||
1. /AGENTS.md — правила работы, стиль, формат финального ответа.
|
|
||||||
2. /docs/tasks/TASK_tgu-editor-integration.md — полное ТЗ этой задачи (этот файл).
|
|
||||||
3. Исходный прототип: /docs/ui-prototypes/pcp-tgu-editor/ (editor.jsx и соседние).
|
|
||||||
4. Целевую структуру: services/pcp-tgu-ops-ui/src (App.tsx, features/, api/, styles/).
|
|
||||||
|
|
||||||
Это ПОРТ, а не копирование: прототип на Babel-standalone с глобалями и мок-данными;
|
|
||||||
цель — ESM/TS-модули, реальный API-клиент, существующая тема и паттерны проекта.
|
|
||||||
|
|
||||||
Выполняй строго по этапам из раздела 5 ТЗ. Делай минимальные ревьюопригодные изменения,
|
|
||||||
не трогай чужие сервисы и backend (Kotlin). Карту, орбитальную математику и мок-данные
|
|
||||||
из прототипа НЕ переноси — переиспользуй features/tgu-map-2d и api/tguApi (раздел 3).
|
|
||||||
После каждого этапа прогоняй `npm run test` и `npm run build` в модуле.
|
|
||||||
|
|
||||||
В конце дай отчёт в формате из раздела «Обязательный формат финального ответа» AGENTS.md.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Контекст и цель
|
|
||||||
|
|
||||||
`pcp-tgu-ops-ui` — операторский UI ТГУ (Mission Ops): уже работающее SPA на **Vite 7 + React 19 + TypeScript**, feature-sliced структура (`features/tgu-planning`, `features/tgu-map-2d`), централизованный API-клиент `src/api/tguApi.ts` (все вызовы через `/api/...`, dev-proxy на backend `:7008`), токены темы в `src/styles/theme.css`, юнит-тесты на Vitest.
|
|
||||||
|
|
||||||
Прототип «Редактор» — браузерный demo (React/Babel из CDN, JSX компилируется в браузере, общие глобали `D`/`SENSOR_MODES`/`window.WorldGeo`, мок-данные из `data.jsx`). Это **другой стек**, поэтому его нельзя «подключить» файлами — нужно портировать как новую фичу приложения.
|
|
||||||
|
|
||||||
**Цель:** редактор включений КА доступен как третья вкладка приложения (`timeline | map | editor`), работает на реальных данных через `tguApi`, переиспользует существующую карту и тему, покрыт тестами и собирается без ошибок.
|
|
||||||
|
|
||||||
Функциональность редактора (из прототипа `editor.jsx`): выбор активного КА/плана, черновик включений (`works`: `shoot` — съёмка, `downlink` — сброс), drag/resize по таймлайну, добавление съёмки по точке на карте и сброса по сеансу со станцией, контекст соседних КА, undo/redo, проверка конфликтов (пересечения по времени — живые; видимость цели/станции — по кнопке), применение правок.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Границы задачи (scope)
|
|
||||||
|
|
||||||
**В scope:**
|
|
||||||
- Новая фича `src/features/tgu-editor/` в `pcp-tgu-ops-ui`.
|
|
||||||
- Третья вкладка `"editor"` в существующем переключателе вкладок.
|
|
||||||
- Перенос UI-компонентов редактора на TSX + типизация.
|
|
||||||
- Чистые модули логики (undo/redo, конфликты) + их тесты.
|
|
||||||
- Подключение к реальным данным через `tguApi` (планы/платформы).
|
|
||||||
- Перемаппинг токенов темы.
|
|
||||||
|
|
||||||
**Вне scope (НЕ делать без отдельного запроса):**
|
|
||||||
- Изменения в любых Kotlin-сервисах, включая `pcp-tgu-service` (backend-эндпоинт сохранения правок — отдельная задача, см. раздел 7).
|
|
||||||
- Перенос карты/орбитальной математики/мок-данных из прототипа (раздел 3).
|
|
||||||
- Добавление роутера, state-менеджеров, UI-библиотек, новых рантайм-зависимостей без необходимости.
|
|
||||||
- Рефакторинг существующих фич `tgu-planning` / `tgu-map-2d` сверх минимально необходимого для переиспользования.
|
|
||||||
- Изменения `helm/`, `settings.gradle.kts`, CI (модуль фронта в них и так не участвует).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Целевая архитектура и правила переиспользования
|
|
||||||
|
|
||||||
Следуй существующему feature-sliced паттерну (см. `features/tgu-planning`, `features/tgu-map-2d`): UI-компоненты в `*.tsx`, типы в `model/*.ts`, нетривиальная логика — в отдельных чистых `*.ts` модулях с тестами.
|
|
||||||
|
|
||||||
**Обязательно переиспользовать (НЕ дублировать):**
|
|
||||||
- **Карта** — `features/tgu-map-2d` (`Tgu2DMapView`, `canvas/drawTracks|drawSwath|drawStations|drawPlanWorks|drawBaseMap`, `geometry/mapProjection|spatialIndex|bbox`). Редактору нужна карта в режиме «выбор точки съёмки» и подсветки трасс — расширяй существующий компонент пропсами (`pickMode`, `onPickPoint`, `targets`), а не переноси `mapview.jsx`/`orbit.jsx`/`world.js`/`basemap.js`.
|
|
||||||
- **API** — `src/api/tguApi.ts` (`fetchPlans`, `fetchPlatforms`, паттерн `fetchJson`/`TguApiError`). Все сетевые вызовы редактора добавляй сюда же или в `features/tgu-editor/editorApi.ts` в том же стиле.
|
|
||||||
- **Тема** — `src/styles/theme.css` (см. раздел 6).
|
|
||||||
- **Модели** — `src/model/tguTypes.ts`, `src/model/timelineTypes.ts`. Переиспользуй существующие типы планов/платформ, не вводи параллельные.
|
|
||||||
|
|
||||||
**Не переносить из прототипа:** `mapview.jsx`, `orbit.jsx`, `world.js`, `basemap.js`, `util.jsx` (дублируют то, что уже есть в TS), `data.jsx` (мок), `tabs.jsx` (заменяется вкладкой), `Editor.html`/`Map.html` (точка входа уже `index.html` + `main.tsx`).
|
|
||||||
|
|
||||||
Орбитальные функции, которых нет в `tgu-map-2d` (`targetPasses`, `stationPasses`, `subPoint`, `nearestStation`), вынеси в чистый общий модуль `features/tgu-map-2d/geometry/passes.ts` (с тестами) и переиспользуй из карты и редактора. Не размещай бизнес/гео-логику внутри React-компонентов.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Инвентаризация и маппинг файлов
|
|
||||||
|
|
||||||
Источник: `docs/ui-prototypes/pcp-tgu-editor/` (положить туда содержимое архива до начала работ — по образцу уже существующих `docs/ui-prototypes/pcp-tgu-mission-ops/`).
|
|
||||||
|
|
||||||
| Прототип | Действие | Целевой файл |
|
|
||||||
|---|---|---|
|
|
||||||
| `editor.jsx` (`EditorApp`) | Портировать, убрать `ReactDOM.createRoot`, сделать экспортируемым компонентом-вкладкой | `features/tgu-editor/TguEditorTab.tsx` |
|
|
||||||
| `editor.jsx` (`EditorToolbar`) | Портировать | `features/tgu-editor/EditorToolbar.tsx` |
|
|
||||||
| `editorrail.jsx` | Портировать | `features/tgu-editor/EditorRail.tsx` |
|
|
||||||
| `editortimeline.jsx` | Портировать | `features/tgu-editor/EditorTimeline.tsx` |
|
|
||||||
| `editorpanels.jsx` (`WorkInspector`/`ShootPanel`/`DownlinkPanel`) | Портировать | `features/tgu-editor/EditorPanels.tsx` |
|
|
||||||
| `visibility.jsx` | Портировать визуализацию; гео-расчёты вынести в `passes.ts` | `features/tgu-editor/VisibilityTracks.tsx` |
|
|
||||||
| `editor.jsx` (undo/redo, mutate, seedDraft) | Вынести в чистый модуль | `features/tgu-editor/editorDraft.ts` (+`editorDraft.test.ts`) |
|
|
||||||
| `editor.jsx` (overlaps + runCheck) | Вынести в чистый модуль | `features/tgu-editor/editorConflicts.ts` (+`editorConflicts.test.ts`) |
|
|
||||||
| типы `work`/`draft`/`target` | Создать | `features/tgu-editor/model/editorTypes.ts` |
|
|
||||||
| `mapview.jsx`, `orbit.jsx`, `world.js`, `basemap.js`, `util.jsx`, `data.jsx`, `tabs.jsx`, `*.html` | НЕ переносить | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Пошаговый план (атомарные этапы)
|
|
||||||
|
|
||||||
Каждый этап завершай прогоном `npm run test` и `npm run build` в `services/pcp-tgu-ops-ui`.
|
|
||||||
|
|
||||||
**Этап 0 — подготовка.** Скопировать прототип в `docs/ui-prototypes/pcp-tgu-editor/`. Прочитать `App.tsx`, `TguPlanningPage.tsx`, `TguToolbar.tsx`, `tguApi.ts`, `theme.css`, `Tgu2DMapView.tsx`. Зафиксировать в отчёте точки расширения.
|
|
||||||
|
|
||||||
**Этап 1 — типы и каркас фичи.** Создать `features/tgu-editor/model/editorTypes.ts` (`Work` с `kind: "shoot" | "downlink"`, `origin`, `modeId`, `stationId`, `targetLat/Lon`, `isNew`; `Draft`, `EditorTarget`). Создать пустой `TguEditorTab.tsx` с заголовком-заглушкой. Скомпилироваться.
|
|
||||||
|
|
||||||
**Этап 2 — чистая логика + тесты.** Перенести в `editorDraft.ts`: `seedDraft`, `mutate`, `undo`, `redo`, `addShoot`, `addDownlink`, `updateWork`, `deleteWork`, `duplicateWork` (как чистые функции над `Draft`/`Work`, без React и без `Date.now()` внутри — id/время передавать параметром для детерминизма тестов). Перенести в `editorConflicts.ts`: `overlaps` и проверку видимости. Написать `editorDraft.test.ts` и `editorConflicts.test.ts` (undo/redo инварианты, обнаружение пересечений, отсутствие/наличие видимости). Это первоочередное — логика должна быть покрыта до UI.
|
|
||||||
|
|
||||||
**Этап 3 — токены темы.** Реализовать перемаппинг (раздел 6). Убедиться, что портируемые стили резолвятся в палитру приложения.
|
|
||||||
|
|
||||||
**Этап 4 — UI без карты.** Портировать `EditorToolbar`, `EditorRail`, `EditorTimeline`, `EditorPanels`, `VisibilityTracks` на TSX. Завязать их на состояние в `TguEditorTab` (`useState`/`useMemo`/`useCallback` напрямую из `react`). Drag/resize таймлайна сохранить. Вместо `alert()` использовать уведомление в стиле `decisionNotice`/`decisionError`.
|
|
||||||
|
|
||||||
**Этап 5 — карта.** Расширить `Tgu2DMapView` пропсами `pickMode`/`onPickPoint`/`targets` (минимально, без поломки текущего использования в `Tgu2DMapTab`). Подключить в редакторе для выбора точки съёмки и подсветки трасс цели/станции через `passes.ts`.
|
|
||||||
|
|
||||||
**Этап 6 — данные.** Заменить мок `D` на реальные данные: планы/платформы грузить через `tguApi` (как в `TguPlanningPage`). `seedDraft` строить из выбранного плана. Активный КА/план брать из пропсов (раздел 7), не из `location.search`.
|
|
||||||
|
|
||||||
**Этап 7 — вкладка.** В `model/timelineTypes.ts`/`TguPlanningPage.tsx` расширить `activeTab` до `"timeline" | "map" | "editor"`, добавить кнопку вкладки в `TguToolbar` (`onTabChange`), отрендерить `TguEditorTab` при `activeTab === "editor"`, передав `selectedSpacecraftId`/`selectedPlan`/`appliedRange`.
|
|
||||||
|
|
||||||
**Этап 8 — apply (заглушка).** Реализовать `applyDraft` через типизированную функцию `saveEditedPlan` в `editorApi.ts`, помеченную `// TODO(backend): эндпоинт сохранения правок плана отсутствует в pcp-tgu-service` (раздел 7). Поведение: показать понятное уведомление о том, что сохранение во внешний сервис ещё не реализовано; не падать.
|
|
||||||
|
|
||||||
**Этап 9 — финализация.** Прогнать `npm run test` и `npm run build`. Отчёт по формату AGENTS.md.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Перемаппинг токенов темы
|
|
||||||
|
|
||||||
Прототип использует oklch-токены (`--bg-0..3`, `--ink..--ink-3`, `--t-optical/radar/combo`, `--now`, `--warn`, `--line`, `--shadow`), которых нет в `theme.css` приложения (там hex: `--bg-base`, `--bg-panel`, `--text`, `--accent`, `--danger`, `--warning`, ...).
|
|
||||||
|
|
||||||
Минимально-инвазивный подход: создать `src/styles/editor-tokens.css` (импортировать в `TguEditorTab` или в `main.tsx`) с алиасами, чтобы портированный код, ссылающийся на старые имена, резолвился в палитру приложения — без переписывания каждого inline-стиля:
|
|
||||||
|
|
||||||
```css
|
|
||||||
:root {
|
|
||||||
--bg-0: var(--bg-base);
|
|
||||||
--bg-1: var(--bg-panel);
|
|
||||||
--bg-2: var(--bg-panel-2);
|
|
||||||
--bg-3: var(--bg-elevated);
|
|
||||||
--ink: var(--text);
|
|
||||||
--ink-1: var(--text);
|
|
||||||
--ink-2: var(--text-muted);
|
|
||||||
--ink-3: var(--text-dim);
|
|
||||||
--now: var(--warning);
|
|
||||||
--warn: var(--warning);
|
|
||||||
/* --line, --line-soft, --accent, --shadow совпадают по имени — проверить значения */
|
|
||||||
/* --t-optical/radar/combo: задать на основе --accent/--warning/--danger или вынести в палитру типов КА */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Уточнить фактические значения `--line/--line-soft/--accent/--shadow` (имена совпадают, значения могут отличаться) и зафиксировать решение в отчёте. Альтернатива (если команда предпочитает) — переписать стили редактора сразу на родные токены приложения; выбрать один путь и не смешивать.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Данные и API
|
|
||||||
|
|
||||||
- **Чтение:** планы и платформы — через существующие `fetchPlans`/`fetchPlatforms` (`tguApi.ts`). Активный КА/план редактор получает пропсами от `TguPlanningPage` (`selectedSpacecraftId`, `selectedPlan`), а не из URL.
|
|
||||||
- **Гео/орбиты:** через `tgu-map-2d` + `passes.ts`; мок `world.js`/`data.jsx` не использовать.
|
|
||||||
- **Сохранение правок (apply):** в текущем API `pcp-tgu-service` **нет** эндпоинта сохранения отредактированного набора включений (есть только выдача плана и `decision`/`issue`). Поэтому в этой задаче `applyDraft` реализуется как заглушка с явным `TODO(backend)` и не делает реальной записи.
|
|
||||||
|
|
||||||
**Предложение контракта для backend-команды** (вне scope, зафиксировать в отчёте, не реализовывать):
|
|
||||||
- `POST /api/tgu-planning/plans/{planId}/edits` с телом `{ works: EditedWork[], baseRevision?: string }`, идемпотентно по `operationId`, возвращает новый план/attempt. Семантику (новый attempt vs патч существующего) согласовать отдельно.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Конвенции
|
|
||||||
|
|
||||||
Следуй `AGENTS.md` (раздел про стиль и scope) применительно к TS/React:
|
|
||||||
- Минимальные, локальные, ревьюопригодные изменения; не переименовывать и не форматировать несвязанные файлы.
|
|
||||||
- Предпочитать `const`, маленькие функции, явные имена; не вводить «умные» абстракции на будущее.
|
|
||||||
- Бизнес/гео-логику не держать в React-компонентах — выносить в чистые `*.ts` модули.
|
|
||||||
- Комментарии — там, где помогают сопровождению (undo/redo инварианты, правила конфликтов, edge cases), а не на каждую строку. Язык комментариев — как в проекте.
|
|
||||||
- Не глотать ошибки сети молча — использовать паттерн `TguApiError`/уведомлений, как в существующем коде.
|
|
||||||
- Не добавлять новые рантайм-зависимости без явной необходимости; React/ReactDOM/Babel из CDN прототипа не тащить — они уже есть в проекте как npm-пакеты.
|
|
||||||
- TypeScript: без `any` в публичных сигнатурах; типы — в `model/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Критерии приёмки и команды валидации
|
|
||||||
|
|
||||||
Рабочая директория: `services/pcp-tgu-ops-ui`.
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install # при необходимости
|
|
||||||
npm run test # vitest run — все тесты зелёные, включая новые editorDraft/editorConflicts/passes
|
|
||||||
npm run build # tsc -b && vite build — без ошибок типов и сборки
|
|
||||||
npm run dev # ручная проверка: вкладка «Редактор» открывается, см. ниже
|
|
||||||
```
|
|
||||||
|
|
||||||
Функциональные критерии:
|
|
||||||
1. В тулбаре доступна третья вкладка; переключение `timeline | map | editor` работает, существующие вкладки не сломаны.
|
|
||||||
2. Редактор открывается на реальных данных (планы/платформы из `tguApi`), без мок-`D`.
|
|
||||||
3. Работают: drag/resize включений, добавление съёмки по точке на карте, добавление сброса по станции, undo/redo, проверка конфликтов (живые пересечения + видимость по кнопке).
|
|
||||||
4. Карта в редакторе — это `tgu-map-2d` (нет второго движка карты в бандле).
|
|
||||||
5. Тема консистентна с приложением (нет «бесцветных» из-за отсутствующих токенов).
|
|
||||||
6. `applyDraft` не падает и явно сообщает про отсутствующий backend-эндпоинт.
|
|
||||||
|
|
||||||
Если какая-то команда не запускалась — указать какая и почему, и описать остаточный риск (по AGENTS.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Риски, допущения, чего не делать
|
|
||||||
|
|
||||||
- **Допущение:** структура планов/включений из `tguApi` достаточна, чтобы построить `seedDraft`. Если в реальном ответе нет полей под `works` редактора — зафиксировать gap, сделать адаптер-маппер и описать недостающие поля в отчёте, **не** менять backend.
|
|
||||||
- **Риск:** React 19 + StrictMode даёт двойной вызов эффектов — проверить `runCheck`/`setTimeout` на отсутствие двойных срабатываний.
|
|
||||||
- **Риск:** расширение `Tgu2DMapView` может задеть `Tgu2DMapTab` — новые пропсы делать опциональными с дефолтами, существующее поведение не менять.
|
|
||||||
- **Не делать:** правки в Kotlin-сервисах, в чужих фичах сверх необходимого, новый роутинг, новые тяжёлые зависимости, перенос мок-данных и второго движка карты.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Обязательный формат финального ответа
|
|
||||||
|
|
||||||
По `AGENTS.md` агент завершает задачу блоками:
|
|
||||||
|
|
||||||
1. **Changed files** — список изменённых/созданных файлов.
|
|
||||||
2. **What changed** — краткое summary реализации по этапам.
|
|
||||||
3. **Architecture compliance** — почему соблюдены границы слоёв (логика вне компонентов, переиспользование `tgu-map-2d`/`tguApi`/темы, отсутствие дублей карты и мок-данных).
|
|
||||||
4. **Validation** — какие команды (`npm run test`, `npm run build`, ручная проверка) запускались и результаты.
|
|
||||||
5. **Risks / assumptions** — что не проверено, gap по данным/backend, edge cases.
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
# Задача: pcp-tgu-service — «храним только выданное» + закладочный флоу
|
|
||||||
|
|
||||||
> ТЗ для кодинг-агента и для приёмки. Раздел 0 — промпт запуска.
|
|
||||||
> Разделы 1–9 — что и как делаем. Раздел 10 — **критерии приёмки** (по ним проверяем результат).
|
|
||||||
> Стиль работы, границы слоёв и формат финального ответа — по корневому `AGENTS.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Промпт для запуска агента (copy-paste)
|
|
||||||
|
|
||||||
```
|
|
||||||
Ты работаешь в монорепозитории observatio-terrae (pcp), модуль services/pcp-tgu-service (Kotlin/Spring Boot).
|
|
||||||
|
|
||||||
Задача: перестроить хранение и выдачу планов ТГУ — перестать материализовать
|
|
||||||
будущую цепочку планов в БД (хранить только выданные планы и их историю — вечно),
|
|
||||||
считать будущее на лету как проекцию, и добавить закладочный флоу с реакцией
|
|
||||||
на два Kafka-события «не заложен» и «не принят».
|
|
||||||
|
|
||||||
Перед началом обязательно прочитай:
|
|
||||||
1. /AGENTS.md — правила работы, стиль, формат финального ответа.
|
|
||||||
2. /docs/tasks/TASK_tgu-plan-on-demand-redesign.md — это ТЗ.
|
|
||||||
3. Текущий код: services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/
|
|
||||||
(service/, entity/, repository/, controller/, integration/kafka/, config/).
|
|
||||||
|
|
||||||
Делай строго по фазам раздела 5. Всё — в одной ветке/одном PR.
|
|
||||||
Ядро расчёта цепочки (PlanCalculationService) НЕ меняем — оно уже монотонно-корректно.
|
|
||||||
После работы прогоняй ./gradlew :services:pcp-tgu-service:test из корня монорепо.
|
|
||||||
В конце дай отчёт в формате из AGENTS.md и пройдись по разделу 10 (критерии приёмки).
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Контекст и цель
|
|
||||||
|
|
||||||
`pcp-tgu-service` (порт `:7011`) — роллинг-планировщик сеансов управления по каждому операционному КА. Поток: список КА из НСИ → окна видимости (ЗРВ) из баллистики → расчёт цепочки планов (~`target` 1440±240 мин, привязка 1:1 к ЗРВ, строгая монотонность) → выдача дозревшего плана через короткий BPMN → решение из Kafka. UI (через агрегатор `:7008`) рисует таймлайн планов.
|
|
||||||
|
|
||||||
**Проблема сейчас.** Таблица `planned_plan` материализует **всю будущую цепочку** как `PLANNED`-строки и переписывает её на каждое изменение хэша ЗРВ. Это копит «лишнее»: пачки `SUPERSEDED`/`EXPIRED` на каждый пересчёт, рост без границ, отдельная машинерия `plan_recalculation_job`, и целый класс багов рассинхрона цепочки со снапшотом («схлопывание в SUPERSEDED», «зомби-PLANNED»). При этом будущая цепочка — **детерминированная проекция** от (снапшот ЗРВ + базовый план + конфиг).
|
|
||||||
|
|
||||||
**Цель.**
|
|
||||||
1. Хранить в `planned_plan` **только планы, перешедшие в выдачу**, и их историю — **вечно** (ретеншн не делаем).
|
|
||||||
2. Будущее (не выданное) — **не хранить вовсе**, считать на лету как проекцию (в т.ч. для UI).
|
|
||||||
3. Добавить **закладочный флоу**: после `ACCEPTED` план оптимистично считается заложенным; реагируем только на явный негатив — два раздельных Kafka-события «не заложен» и «не принят», оба пока ведут себя одинаково (срочно выдать план на ближайшую ЗРВ), но разнесены для будущего расхождения.
|
|
||||||
4. Реагировать на **изменение параметров станций** (Kafka-событие): станция могла выйти из строя — нужно заново запросить ЗРВ у баллистики по затронутым КА и пересобрать цепочку. В on-demand-модели это сводится к форсу `visibility_refresh_job` → новый снапшот → проекция пересчитается сама.
|
|
||||||
5. Снести мёртвый код (`tracked_plan`) и ставшую ненужной машинерию пересчёта (`plan_recalculation_job`).
|
|
||||||
|
|
||||||
Масштаб: **100–150 КА**, горизонт планирования **до 10 суток**, деплой **2–3 реплики** (горизонтальное масштабирование уже обеспечено координацией через БД: `FOR UPDATE SKIP LOCKED` + pessimistic-lock на КА).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Границы задачи (scope)
|
|
||||||
|
|
||||||
**В scope:**
|
|
||||||
- Перестроение хранения/выдачи планов в `pcp-tgu-service` (backend, Kotlin).
|
|
||||||
- Новый сервис проекции; переписанный воркер выдачи; read-эндпоинт, отдающий persisted ∪ проекцию.
|
|
||||||
- Новые статусы плана и закладочный флоу с двумя новыми Kafka-консьюмерами.
|
|
||||||
- Удаление мёртвого кода и снятие `plan_recalculation_job`.
|
|
||||||
- Миграции БД, изменения конфига (`application.yml` + `config-repo/pcp-tgu-service.yaml`).
|
|
||||||
- Юнит-тесты на новую логику.
|
|
||||||
|
|
||||||
**Вне scope (без отдельного запроса):**
|
|
||||||
- Фронтенд `pcp-tgu-ops-ui` и агрегатор `pcp-ui-service` — контракт `/api/plans` сохраняем неизменным, фронт не трогаем.
|
|
||||||
- Изменение ядра расчёта `PlanCalculationService` (кроме переиспользования; алгоритм не меняем).
|
|
||||||
- BPMN-схемы (`issueSatellitePlan`, `updateAcceptedPlanSlots`) — не меняем.
|
|
||||||
- Источник/продюсер новых Kafka-событий «не заложен»/«не принят» — мы только консьюмеры; контракт ниже фиксируем.
|
|
||||||
- Ретеншн/архивация — НЕ делаем (выданное храним вечно).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Текущая архитектура (точки изменения)
|
|
||||||
|
|
||||||
- [PlannedPlanEntity](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlannedPlanEntity.kt) / `planned_plan` — хранит всю цепочку, upsert по бизнес-ключу `(spacecraft, start, end, kpp)`.
|
|
||||||
- [RebuildPlanScheduleUseCase](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/RebuildPlanScheduleUseCase.kt) — пересобирает цепочку: `calculatePlans/After` → upsert → `supersedeObsoleteFuturePlans` → `expireMissedPlans`.
|
|
||||||
- [PlanCalculationService](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanCalculationService.kt) — **ядро, не трогаем**.
|
|
||||||
- [SpacecraftVisibilitySnapshotService](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/SpacecraftVisibilitySnapshotService.kt) — на изменение хэша ставит `plan_recalculation_job`.
|
|
||||||
- [PlanRecalculationWorker](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanRecalculationWorker.kt) / `PlanRecalculationJobService` / `plan_recalculation_job` — **снимаем целиком**.
|
|
||||||
- [IssueDuePlanWorker](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/IssueDuePlanWorker.kt) — сейчас сканирует `PLANNED` (`findDueForUpdate`). Переписываем на on-demand.
|
|
||||||
- [HandlePlanDecisionUseCase](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt) — обработка `ACCEPTED/REJECTED`; `promoteNextPlanOrEnqueueRebuild` завязан на хранимые `PLANNED` → переписываем.
|
|
||||||
- [PlanQueryService](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryService.kt) / [PlanController](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlanController.kt) — read-путь, дополняем проекцией.
|
|
||||||
- Мёртвый код: [TrackedPlanEntity](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/TrackedPlanEntity.kt), `TrackedPlanStatus`, `TrackedPlanSource`, `TrackedPlanJpaRepository` — на репозиторий нет ссылок.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Целевая архитектура
|
|
||||||
|
|
||||||
- `planned_plan` пополняется строкой **только в момент выдачи** (статус `ISSUING`). Дальше строка живёт по жизненному циклу выдачи и **хранится вечно**.
|
|
||||||
- **Будущее не хранится.** `PlanProjectionService.project(spacecraftId)` считает цепочку на лету из последнего снапшота ЗРВ + текущей базы + конфига, переиспользуя `PlanCalculationService`.
|
|
||||||
- **Выдача** ([IssueDuePlanWorker]) идёт по активным КА, считает голову проекции; если голова «дозрела» (`issue_not_before ≤ now`) или взведён `urgent_reissue_from` — материализует **ровно эту голову** в `ISSUING` и выдаёт. Никаких `PLANNED`-строк в БД.
|
|
||||||
- **Read** ([PlanQueryService]) отдаёт `persisted (вся история) ∪ проекция будущего` (виртуальный `PLANNED`, в БД не пишется), дедуп по бизнес-ключу. Контракт `/api/plans` неизменен.
|
|
||||||
- **Закладочный флоу**: `ACCEPTED` → `AWAITING_LAYIN` (оптимистично = база, цепочка идёт дальше). Два новых события гасят план терминально (`LAYIN_FAILED` / `NOT_ACCEPTED`) и взводят срочную перевыдачу на ближайшую ЗРВ с лидом `now + reissue-prep-minutes`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Фазы работ
|
|
||||||
|
|
||||||
### Фаза 0. Чистка мёртвого кода
|
|
||||||
- Удалить `TrackedPlanEntity`, `TrackedPlanStatus`, `TrackedPlanSource`, `TrackedPlanJpaRepository`.
|
|
||||||
- Миграция `V7__drop_tracked_plan.sql`: `DROP TABLE IF EXISTS tracked_plan;`
|
|
||||||
- Убедиться, что компиляция/тесты не ссылались на эти классы.
|
|
||||||
|
|
||||||
### Фаза 1. On-demand проекция, конец материализации цепочки
|
|
||||||
- Новый `PlanProjectionService` с `project(spacecraftId): List<CalculatedPlan>` и `projectHead(spacecraftId): CalculatedPlan?`:
|
|
||||||
читает `spacecraft_visibility_snapshot`, текущую базу (`spacecraft_planning_state.currentBasePlanId`), считает цепочку через `PlanCalculationService` (`calculatePlans`/`calculatePlansAfter`). Опционально — in-memory кэш по `payload_hash` (если делаешь — инвалидируй при смене снапшота/базы; при 150 КА можно и без кэша).
|
|
||||||
- Переписать `IssueDuePlanWorker` на on-demand: идём по активным КА (`spacecraft_snapshot.active = true` / `spacecraft_planning_state`), для каждого через `PlanProjectionService.projectHead` берём ближайший план; если у КА нет активной попытки (`activeAttemptId == null`) и (`issue_not_before(head) ≤ now` **или** взведён `urgent_reissue_from`) — материализуем голову в `planned_plan` со статусом `ISSUING` и стартуем выдачу. Сохраняем claim-семантику с lock на КА.
|
|
||||||
- `RebuildPlanScheduleUseCase`: убрать материализацию (`upsertPlannedByBusinessKey`, `supersedeObsoleteFuturePlans`, `expireMissedPlans`). Логику расчёта забирает `PlanProjectionService`; сам use case либо удаляется, либо превращается в тонкую обёртку над проекцией (если на него ссылается `TestTguController` — переориентировать/упростить).
|
|
||||||
- **Снять машинерию пересчёта**: удалить `PlanRecalculationWorker`, `PlanRecalculationJobService`, `PlanRecalculationJobEntity`, `PlanRecalculationJobRepository`; убрать `enqueue(...)` из `SpacecraftVisibilitySnapshotService` (hash-проверку оставить — она просто перестаёт что-либо ставить). Миграция `V8__drop_plan_recalculation_job.sql`: `DROP TABLE IF EXISTS plan_recalculation_job;`
|
|
||||||
- `PlannedPlanRepository`: удалить ставшие мёртвыми `upsertPlannedByBusinessKey`, `findAllBySpacecraftIdAndStatusAndEndTimeAfterOrderByStartTime`, `findAllBySpacecraftIdAndStatusAndStartTimeLessThanEqualOrderByStartTime`, `findTopBySpacecraftIdAndStatusAndStartTimeAfterOrderByStartTime`. Оставить/добавить: `lockByPlanId`, выборка истории по КА и по всем для read, защита от дублей выдачи.
|
|
||||||
|
|
||||||
### Фаза 2. Read-проекция для UI
|
|
||||||
- `PlanQueryService.getAllPlans/getPlansBySpacecraftId`: вернуть `persisted ∪ projection`. Persisted — все строки `planned_plan` (история вечна; `historyDays` оставить как фильтр **отображения** левого края, БД не режем). Projection — `PlanProjectionService.project(...)` по активным КА, виртуальный статус `PLANNED`, дедуп: проекционный план не добавляем, если его бизнес-ключ уже есть среди persisted.
|
|
||||||
- `PlanResponse`: научить представлять виртуальные планы (детерминированный `planId` из бизнес-ключа, либо nullable — на усмотрение, но контракт JSON для фронта не ломать; `status = PLANNED`). Добавить фабрику `from(CalculatedPlan, status)`.
|
|
||||||
|
|
||||||
### Фаза 3. Статусы и закладочный флоу
|
|
||||||
- `PlannedPlanStatus`: добавить `AWAITING_LAYIN`, `LAID_IN`, `LAYIN_FAILED`, `NOT_ACCEPTED`. `PLANNED` оставить только как виртуальный для проекции (в БД не писать).
|
|
||||||
- `HandlePlanDecisionUseCase` (топик `satellite-plan-decision`):
|
|
||||||
- `ACCEPTED` → план `ACCEPTED` затем `AWAITING_LAYIN`, `currentBasePlanId = plan`, проекция продолжается; `acceptedPlanSlotsUpdateJobService.enqueue(plan.planId)` (как есть).
|
|
||||||
- `REJECTED` → план терминально `REJECTED` → общий `reissueOnNearestWindow(...)`.
|
|
||||||
- Удалить `promoteNextPlanOrEnqueueRebuild` (он завязан на хранимые `PLANNED`).
|
|
||||||
- Новые события (раздельные консьюмеры, общее поведение):
|
|
||||||
- `TopicsProperties`: добавить `layinFailed` и `planNotAccepted`.
|
|
||||||
- DTO `PlanExecutionFeedbackMessage(eventId, spacecraftId, planId, occurredAt, reason)` — пока общий на оба события.
|
|
||||||
- Консьюмеры `LayinFailedConsumer`, `PlanNotAcceptedConsumer` (groupId `${spring.kafka.consumer.group-id}`).
|
|
||||||
- В use case — два метода `handleLayinFailed(msg)` / `handleNotAccepted(msg)`, **оба идемпотентны** по `eventId` через `processed_plan_decision_event`, оба зовут общий `reissueOnNearestWindow(...)`; статус помечаемого плана соответственно `LAYIN_FAILED` / `NOT_ACCEPTED`.
|
|
||||||
- `reissueOnNearestWindow(spacecraftId, failedPlanId, now)` (общий для REJECTED-после-cutoff / LAYIN_FAILED / NOT_ACCEPTED):
|
|
||||||
- под lock на КА: помечает план терминальным статусом (история — навечно), снимает его с роли базы (`currentBasePlanId` ← предыдущая валидная база или `null`), чистит `activePlanId/activeAttemptId`;
|
|
||||||
- ставит `spacecraft_planning_state.urgent_reissue_from = now + reissue-prep-minutes`.
|
|
||||||
- `IssueDuePlanWorker` urgent-path: если у КА `urgent_reissue_from != null` — голову проекции берём как ближайшую ЗРВ со `startTime ≥ urgent_reissue_from`, материализуем и выдаём немедленно (не ждём `issue_not_before`), после успешного старта `urgent_reissue_from = null`.
|
|
||||||
- Миграция `V9__planning_state_urgent_reissue.sql`: `ALTER TABLE spacecraft_planning_state ADD COLUMN IF NOT EXISTS urgent_reissue_from TIMESTAMP NULL;`
|
|
||||||
|
|
||||||
### Фаза 3b. Событие «изменились параметры станций»
|
|
||||||
- `TopicsProperties` + конфиги: `topics.station-params-changed: pcp.tgu.station-params-changed.v1`.
|
|
||||||
- DTO `StationParamsChangedMessage(eventId, stationId?, occurredAt, reason?)`.
|
|
||||||
- Консьюмер `StationParamsChangedConsumer` (groupId `${spring.kafka.consumer.group-id}`).
|
|
||||||
- Use case `handleStationParamsChanged(msg)`: **идемпотентен** по `eventId` (общий ledger `processed_plan_decision_event`); ставит `visibility_refresh_job` (`VisibilityRefreshJobService.enqueue`, reason `STATION_PARAMS_CHANGED`) по **всем активным КА** (`spacecraft_snapshot.active = true`). Дальше [VisibilityRefreshWorker] перечитывает ЗРВ у баллистики → новый снапшот → on-demand проекция пересобирает цепочку автоматически.
|
|
||||||
- `stationId` в событии пока **не используется для сужения** (карты «станция → КА» в tgu нет; баллистика считает ЗРВ по-КА). Поле оставляем на будущее для точечного refresh; сейчас поведение — blanket refresh всех активных КА (при 150 КА дёшево).
|
|
||||||
- Уже выданные/принятые планы (`ISSUING`/`AWAITING_LAYIN`) ретроспективно не меняются — пересобирается только будущая часть цепочки (после базы). Если станция текущего плана отказала по сути — это придёт отдельным негативным событием (`layin-failed`/`not-accepted`).
|
|
||||||
|
|
||||||
### Фаза 4. Конфиг
|
|
||||||
- `PlanningProperties` + `application.yml` + `config-repo/pcp-tgu-service.yaml`:
|
|
||||||
- `external-points-lookahead-days: 3 → 10`;
|
|
||||||
- новый `reissue-prep-minutes: 30`;
|
|
||||||
- `min-reissue-lead-minutes` — оставить (REJECTED-до-cutoff).
|
|
||||||
- `TopicsProperties` + конфиги: `topics.layin-failed: pcp.tgu.layin-failed.v1`, `topics.plan-not-accepted: pcp.tgu.plan-not-accepted.v1`, `topics.station-params-changed: pcp.tgu.station-params-changed.v1` (с возможностью переопределить).
|
|
||||||
|
|
||||||
### Фаза 5. Тесты
|
|
||||||
- Сохранить `PlanCalculationServiceTest` (ядро не тронуто).
|
|
||||||
- Новые: `PlanProjectionServiceTest`, `IssueDuePlanWorker` (on-demand голова + urgent-path), `HandlePlanDecisionUseCase` (`ACCEPTED→AWAITING_LAYIN`; `REJECTED→reissue`), консьюмеры `layin-failed`/`not-accepted` (идемпотентность, раздельность, общий reissue), консьюмер `station-params-changed` (идемпотентность + постановка refresh по всем активным КА), read-проекция (`persisted ∪ projection` + дедуп).
|
|
||||||
- Удалить тесты снятой машинерии (recalc worker, supersede/expire); поправить `RebuildPlanScheduleUseCaseTest`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Модель данных и миграции
|
|
||||||
|
|
||||||
| Миграция | Действие |
|
|
||||||
|---|---|
|
|
||||||
| `V7__drop_tracked_plan.sql` | `DROP TABLE IF EXISTS tracked_plan;` |
|
|
||||||
| `V8__drop_plan_recalculation_job.sql` | `DROP TABLE IF EXISTS plan_recalculation_job;` |
|
|
||||||
| `V9__planning_state_urgent_reissue.sql` | `ALTER TABLE spacecraft_planning_state ADD COLUMN IF NOT EXISTS urgent_reissue_from TIMESTAMP NULL;` |
|
|
||||||
|
|
||||||
`planned_plan` — схему не меняем (новые значения статуса — это `VARCHAR`, миграция не нужна). Индексы оставляем. `ddl-auto: validate` — значит сущности должны точно соответствовать таблицам после миграций.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Конфиг (итоговые значения)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
planning:
|
|
||||||
notification-before-start-minutes: 263
|
|
||||||
target-plan-duration-minutes: 1440
|
|
||||||
target-plan-duration-delta-minutes: 240
|
|
||||||
external-update-enabled: true
|
|
||||||
external-points-lookahead-days: 10 # было 3
|
|
||||||
in-memory-history-days: 14
|
|
||||||
external-update-fixed-delay-ms: 600000
|
|
||||||
notification-check-fixed-delay-ms: 30000
|
|
||||||
min-reissue-lead-minutes: 5
|
|
||||||
reissue-prep-minutes: 30 # новый
|
|
||||||
worker-lock-ttl-seconds: 300
|
|
||||||
|
|
||||||
topics:
|
|
||||||
visibility-windows-changed: pcp.tgu.visibility-windows-changed.v1
|
|
||||||
satellite-plan-decision: pcp.tgu.satellite-plan-decision.v1
|
|
||||||
layin-failed: pcp.tgu.layin-failed.v1 # новый
|
|
||||||
plan-not-accepted: pcp.tgu.plan-not-accepted.v1 # новый
|
|
||||||
station-params-changed: pcp.tgu.station-params-changed.v1 # новый
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Контракты Kafka (входящие, мы — консьюмер)
|
|
||||||
|
|
||||||
Существующий `satellite-plan-decision` (`PlanDecisionMessage`) — без изменений.
|
|
||||||
|
|
||||||
Новые топики, общий DTO `PlanExecutionFeedbackMessage`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"eventId": "uuid", // dedup-ключ (идемпотентность)
|
|
||||||
"spacecraftId": "string",
|
|
||||||
"planId": "uuid", // план, по которому пришёл негатив (текущая база)
|
|
||||||
"occurredAt": "2026-06-02T10:00:00",
|
|
||||||
"reason": "string|null"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `pcp.tgu.layin-failed.v1` — «не заложен».
|
|
||||||
- `pcp.tgu.plan-not-accepted.v1` — «не принят».
|
|
||||||
- Поведение пока идентично; обработчики раздельные.
|
|
||||||
|
|
||||||
Новый топик `pcp.tgu.station-params-changed.v1`, DTO `StationParamsChangedMessage`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"eventId": "uuid", // dedup-ключ (идемпотентность)
|
|
||||||
"stationId": "string|null", // какая станция изменилась; пока не используется для сужения
|
|
||||||
"occurredAt": "2026-06-02T10:00:00",
|
|
||||||
"reason": "string|null"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- При получении — поставить `visibility_refresh_job` по всем активным КА (перезапрос ЗРВ у баллистики), цепочка пересоберётся через on-demand проекцию.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Допущения и риски
|
|
||||||
|
|
||||||
- **Источник «ближайшей ЗРВ»** для срочной перевыдачи — тот же последний снапшот ЗРВ; если в нём нет окна со `startTime ≥ urgent_reissue_from`, голова пуста — КА ждёт следующего refresh окон (это корректное поведение, не ошибка).
|
|
||||||
- **Идемпотентность** новых событий — обязательна (at-least-once Kafka), через общий `processed_plan_decision_event`.
|
|
||||||
- **`ddl-auto: validate`**: после правок прогнать миграции на чистой БД и убедиться, что Hibernate-валидация проходит.
|
|
||||||
- **Совместимость UI**: JSON-форма `/api/plans` не меняется; виртуальные планы отдаются с тем же набором полей и `status: "PLANNED"`.
|
|
||||||
- **Сборка ресурсов**: при правке `application.yml` помнить про `./gradlew :services:pcp-tgu-service:processResources` (IntelliJ-рестарт не всегда пересобирает ресурсы).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Критерии приёмки (по ним проверяем)
|
|
||||||
|
|
||||||
### 10.1. Хранение «только выданное»
|
|
||||||
- [ ] При штатной работе (КА с окнами ЗРВ, ни одного дозревшего плана) в `planned_plan` **нет ни одной строки** для этого КА. Будущее видно только через `/api/plans` как `PLANNED`.
|
|
||||||
- [ ] Строка в `planned_plan` появляется **ровно в момент выдачи** и сразу в статусе `ISSUING` (не `PLANNED`).
|
|
||||||
- [ ] Статус `PLANNED` **никогда не пишется** в БД (проверяемо: `SELECT count(*) FROM planned_plan WHERE status='PLANNED'` = 0 в любой момент).
|
|
||||||
- [ ] Выданные планы и их история **не удаляются** (ретеншн-джоба нет; нет `DELETE`/`SUPERSEDED`-массовой чистки будущего).
|
|
||||||
- [ ] В коде нет материализации цепочки: `upsertPlannedByBusinessKey`, `supersedeObsoleteFuturePlans`, `expireMissedPlans` удалены.
|
|
||||||
|
|
||||||
### 10.2. Проекция будущего
|
|
||||||
- [ ] `GET /api/plans` и `GET /api/plans/{spacecraftId}` возвращают persisted-планы **плюс** вычисленную проекцию будущего; JSON-контракт прежний, фронт работает без изменений.
|
|
||||||
- [ ] Проекция совпадает по содержанию с тем, что раньше материализовалось (та же монотонная цепочка из `PlanCalculationService`): для одних и тех же окон ЗРВ набор `(start,end,kpp)` будущих планов идентичен прежнему поведению.
|
|
||||||
- [ ] Нет дублей: если бизнес-ключ есть в persisted, проекция его не добавляет повторно.
|
|
||||||
- [ ] Левый край истории по-прежнему регулируется `historyDays`.
|
|
||||||
|
|
||||||
### 10.3. Снятие машинерии и мёртвого кода
|
|
||||||
- [ ] Таблиц `tracked_plan` и `plan_recalculation_job` нет (миграции `DROP` применены); связанные сущности/репозитории/воркеры/сервисы удалены.
|
|
||||||
- [ ] `SpacecraftVisibilitySnapshotService` больше не ставит recalc-job; сохранение снапшота и hash-проверка работают.
|
|
||||||
- [ ] Проект компилируется без ссылок на удалённые классы; неиспользуемые методы репозитория убраны.
|
|
||||||
|
|
||||||
### 10.4. Закладочный флоу (оптимизм + реакция на негатив)
|
|
||||||
- [ ] `ACCEPTED` → план переходит в `AWAITING_LAYIN`, становится базой; цепочка продолжается; `updateAcceptedPlanSlots`-job ставится. Никакого ожидания подтверждения закладки в виде доп. строк.
|
|
||||||
- [ ] Отсутствие любых событий после `ACCEPTED` = «заложен» (план остаётся базой, ничего не делаем).
|
|
||||||
- [ ] Событие `pcp.tgu.layin-failed.v1` по базовому плану → план в `LAYIN_FAILED`, взводится `urgent_reissue_from = now + reissue-prep-minutes`, и ближайшая ЗРВ ≥ этого времени **выдаётся немедленно** (не дожидаясь `issue_not_before`).
|
|
||||||
- [ ] Событие `pcp.tgu.plan-not-accepted.v1` → то же поведение, но статус `NOT_ACCEPTED`; обработчик **отдельный** (разные методы/консьюмеры), общее ядро `reissueOnNearestWindow`.
|
|
||||||
- [ ] Оба события **идемпотентны**: повтор того же `eventId` — no-op (повторной перевыдачи нет).
|
|
||||||
- [ ] `reissue-prep-minutes` берётся из конфига (по умолчанию 30); изменение значения меняет лид срочной выдачи.
|
|
||||||
- [ ] Если в снапшоте нет ЗРВ ≥ `urgent_reissue_from` — выдачи нет, флаг сохраняется, повторная попытка произойдёт после обновления окон (без ошибок/исключений в логе).
|
|
||||||
|
|
||||||
### 10.4b. Реакция на изменение параметров станций
|
|
||||||
- [ ] Событие `pcp.tgu.station-params-changed.v1` ставит `visibility_refresh_job` по всем активным КА (reason `STATION_PARAMS_CHANGED`).
|
|
||||||
- [ ] После перезапроса ЗРВ снапшоты обновляются, и `/api/plans` отдаёт цепочку, пересобранную по новым окнам (без перезапуска сервиса и без ручного rebuild).
|
|
||||||
- [ ] Событие идемпотентно по `eventId` (повтор — без повторной постановки refresh).
|
|
||||||
- [ ] Уже выданные/принятые планы ретроспективно не меняются; пересобирается только будущее.
|
|
||||||
|
|
||||||
### 10.5. Масштабирование / корректность под несколькими подами
|
|
||||||
- [ ] Claim во всех воркерах — `FOR UPDATE SKIP LOCKED`; выдача плана и обработка решения — под lock на КА (один план не выдаётся дважды при 2–3 подах).
|
|
||||||
- [ ] У одного КА одновременно не более одной активной попытки выдачи (`activeAttemptId`).
|
|
||||||
- [ ] Сервис стартует и работает с `external-points-lookahead-days: 10`.
|
|
||||||
|
|
||||||
### 10.6. Конфиг и миграции
|
|
||||||
- [ ] `application.yml` и `config-repo/pcp-tgu-service.yaml` содержат итоговые значения раздела 7 (lookahead 10, `reissue-prep-minutes: 30`, два новых топика).
|
|
||||||
- [ ] `ddl-auto: validate` проходит на чистой БД после применения всех миграций (V7–V9).
|
|
||||||
|
|
||||||
### 10.7. Тесты и сборка
|
|
||||||
- [ ] `./gradlew :services:pcp-tgu-service:test` — зелёный.
|
|
||||||
- [ ] Есть тесты на: проекцию, on-demand выдачу, urgent-path, `ACCEPTED→AWAITING_LAYIN`, оба новых консьюмера (идемпотентность + раздельность + общий reissue), read-объединение с дедупом.
|
|
||||||
- [ ] Старые тесты снятой машинерии удалены, оставшиеся — зелёные.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Команды проверки
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# из корня монорепо
|
|
||||||
./gradlew :services:pcp-tgu-service:test
|
|
||||||
./gradlew :services:pcp-tgu-service:processResources # после правок application.yml
|
|
||||||
|
|
||||||
# ручная проверка флоу (нужен tgu.test-controller.enabled=true локально):
|
|
||||||
# - запустить сервис на :7011, дождаться окон ЗРВ
|
|
||||||
# - GET http://localhost:7011/api/plans/{spacecraftId} -> в ответе PLANNED-проекция, в БД строк нет
|
|
||||||
# - сэмулировать ACCEPTED -> в БД ACCEPTED/AWAITING_LAYIN, проекция продолжается
|
|
||||||
# - отправить layin-failed/not-accepted -> терминальный статус + срочная выдача ближайшей ЗРВ
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user