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