# Задача: единообразие времени (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`;
- `` — значение через `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()`, `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) — только представление/парсинг.
- Локализацию под пояс машины «чтобы было привычнее» — это и есть источник проблемы.
```