Files
dc-observatio/docs/tasks/TASK_datetime-utc-consistency.md
T
Дмитрий Соловьев fb0332a030 pcp-tgu-ops-ui: единый UTC во времени; дефолтный диапазон Планы
Время бэка (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 на единообразие времени.
2026-06-02 16:53:48 +03:00

15 KiB
Raw Blame History

Задача: единообразие времени (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.tsparseUtc, 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:

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