Время бэка (naive LocalDateTime и OffsetDateTime) разбиралось через new Date(string)/Date.parse, из-за чего naive-строки трактовались как локальное время браузера: вкладки расходились по времени, а путь создания заявки сдвигал введённое время на офсет. Ввёл единую точку utils/utcTime (parseUtc/formatUtc*/toUtcInputValue/toUtcIso) и перевёл на неё вкладки «Планы», «Заявки», «Карта 2D» — чтение, пикеры диапазона, создание заявки. Плюс на вкладке «Планы» график по умолчанию открывается на интервал сейчас−30 мин … сейчас+3 дня. docs/tasks/TASK_datetime-utc-consistency.md — ТЗ на сверку всей группы сервисов pcp на единообразие времени.
15 KiB
Задача: единообразие времени (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отдаёт планы как naiveLocalDateTime.- Фронт парсил такие строки через
new Date(s)/Date.parse(s), а JS трактует строку без зоны как локальное время браузера → сдвиг на офсет (для МСК — +3 ч). - Одни вкладки показывали через
toLocaleString(пояс браузера), другие — в UTC → одно и то же время выглядело по-разному на разных экранах и у разных операторов. - В пути записи заявки оператор, вводя
01:00, отправлял на бэк22:00.
Корень — не «UTC против местного», а несогласованность парсинга/форматирования и неоднородность формата на проводе между сервисами. Эта задача делает всё единообразным.
2. Целевое соглашение (canonical)
- Всё время — UTC. На любом API времена представляют момент в UTC.
- Формат на проводе — ISO-8601 с явной зоной
Z(предпочтительно). Где сервис исторически отдаёт naiveLocalDateTime, это допустимо только при железном правиле «naive = UTC», но такой эндпоинт — кандидат на миграцию кZ(раздел 6). - Фронт:
- парсинг строк бэка — только через
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.
- парсинг строк бэка — только через
- Никакой неявной локализации под пояс машины ни на фронте, ни в логах/ответах бэка.
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)
Эталон внедрён, но нужно закрепить как инвариант и проверить, что ничего не утекло:
- Найти нарушителей (раздел 7, паттерн A). Любой «голый» разбор/формат строки времени
бэка чинить через
utils/utcTime. - Добавить защиту от регрессий — на выбор:
- юнит-тест-«страж», который грепает
src/и падает на запрещённых паттернах (new Date(<string>),Date.parse(,toLocaleString(безtimeZone,formatDateTimeLocal); или - ESLint-правило (
no-restricted-syntax) с тем же смыслом.
- юнит-тест-«страж», который грепает
- Прогнать
npm run testиnpm run buildв модуле.
6. Backend: когда и что править
По умолчанию — не менять формат на проводе (это контракт). Действия:
- Документировать инвариант. Для сервисов с naive
LocalDateTime: явно зафиксировать «все времена на API — UTC» (KDoc у DTO + запись вdocs/standards, раздел 8). Это дёшево и безопасно, и снимает неоднозначность для будущих клиентов. - Чинить реальные баги. Если найдётся место, где сервис:
- кладёт в naive-поле локальное время сервера (
LocalDateTime.now()вместоLocalDateTime.now(ZoneOffset.UTC)/Instant.now()), или - конвертирует под зону JVM при чтении/записи — это баг: исправить на UTC, добавить тест.
- кладёт в naive-поле локальное время сервера (
- Миграция к явному
Z(опционально, по согласованию). Перевод naiveLocalDateTimeнаOffsetDateTime/Instant(UTC) на границе — самый чистый итог, но это смена контракта: делать только с обновлением потребителей (на фронтеparseUtcуже совместим с обоими) и сериализационным тестом. Оформить как отдельные under-задачи per-service, не «большим взрывом». - Тест формата. Для каждого сервиса, отдающего время, иметь тест, фиксирующий формат на
проводе (есть
Z/ нет зоны) — чтобы контракт не «уехал» молча.
7. Как искать нарушителей (детект-паттерны)
A. Фронт (services/pcp-tgu-ops-ui/src), запрещено вне utils/utcTime.ts:
# разбор строки как локального времени
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-полях:
# 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) — только представление/парсинг.
- Локализацию под пояс машины «чтобы было привычнее» — это и есть источник проблемы.