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

199 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Задача: единообразие времени (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) — только представление/парсинг.
- Локализацию под пояс машины «чтобы было привычнее» — это и есть источник проблемы.
```