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