pcp: единообразие времени (UTC) по группе сервисов

Фронт (pcp-tgu-ops-ui):
- тест-страж utcTime.guard.test.ts: запрещает Date.parse / new Date(string) /
  toLocale* без timeZone вне utils/utcTime; тестовые фикстуры переведены на parseUtc.

Бэкенд:
- UTC-now: LocalDateTime.now() -> LocalDateTime.now(ZoneOffset.UTC) по всем сервисам
  (entity-дефолты, воркеры, use case'ы) и в тестовых фикстурах.
- Конвертации к UTC (баги на не-UTC JVM, каждая с регресс-тестом в не-UTC поясе):
  - tgu: VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase (Instant -> UTC LocalDateTime);
  - mission-planing: MissionPlaningService (OffsetDateTime -> UTC через toUtcLocalDateTime);
  - dynamic-plan: RequestServiceClient (OffsetDateTime -> UTC);
  - slots: SlotRepositoryImpl (getObject(LocalDateTime) вместо getTimestamp().toLocalDateTime()).
- Контрактные сериализационные тесты формата на проводе (naive без зоны / Z):
  PlanResponse (tgu), RequestResponseDto (request, эталон), TguPlanning DTO (ui).
- KDoc-инвариант «=UTC» на UI-facing DTO (tgu PlanResponse, ui-service DTO вкладки ТГУ).

Документация: docs/standards/DATETIME_UTC.md (соглашение, таблица сервисов, под-задачи на миграцию к Z),
docs/standards/CHECKLIST.md (пункт про UTC на API).
This commit is contained in:
Дмитрий Соловьев
2026-06-02 21:51:38 +03:00
parent ac2220d063
commit 260583b134
84 changed files with 760 additions and 166 deletions
@@ -14,6 +14,7 @@ import {
} from "./editorDraft";
import type { PlanMode } from "./editorApi";
import type { DraftHistory } from "./model/editorTypes";
import { parseUtc } from "../../utils/utcTime";
const plan: TguPlanUi = {
planId: "plan-1",
@@ -22,8 +23,8 @@ const plan: TguPlanUi = {
endTime: "2026-05-29T02:00:00Z",
kppId: "KPP-1",
status: "PLANNED",
startMs: Date.parse("2026-05-29T01:00:00Z"),
endMs: Date.parse("2026-05-29T02:00:00Z")
startMs: parseUtc("2026-05-29T01:00:00Z"),
endMs: parseUtc("2026-05-29T02:00:00Z")
};
describe("editorDraft", () => {
@@ -84,8 +85,8 @@ describe("editorDraft", () => {
id: "survey-11",
kind: "shoot",
label: "Съёмка · виток 4321",
startMs: Date.parse("2026-05-29T01:10:00Z"),
endMs: Date.parse("2026-05-29T01:10:00Z") + 180_000,
startMs: parseUtc("2026-05-29T01:10:00Z"),
endMs: parseUtc("2026-05-29T01:10:00Z") + 180_000,
origin: "plan",
targetLat: 55.7,
targetLon: 37.6
@@ -94,8 +95,8 @@ describe("editorDraft", () => {
id: "drop-22",
kind: "downlink",
label: "Сброс · 102",
startMs: Date.parse("2026-05-29T01:40:00Z"),
endMs: Date.parse("2026-05-29T01:40:00Z") + 120_000,
startMs: parseUtc("2026-05-29T01:40:00Z"),
endMs: parseUtc("2026-05-29T01:40:00Z") + 120_000,
origin: "plan",
stationId: "102"
}
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import type { TguPlanUi } from "../../model/timelineTypes";
import { DEFAULT_TGU_2D_MAP_LAYERS } from "./model/mapLayerTypes";
import { getTgu2DMapIntervalState } from "./tgu2DMapInterval";
import { parseUtc } from "../../utils/utcTime";
function plan(startTime: string, endTime: string): TguPlanUi {
return {
@@ -11,16 +12,16 @@ function plan(startTime: string, endTime: string): TguPlanUi {
endTime,
kppId: "KPP-1",
status: "PLANNED",
startMs: Date.parse(startTime),
endMs: Date.parse(endTime)
startMs: parseUtc(startTime),
endMs: parseUtc(endTime)
};
}
describe("tgu2DMapInterval", () => {
it("marks common map interval longer than 7 days as too large", () => {
const state = getTgu2DMapIntervalState({
fromMs: Date.parse("2026-05-29T00:00:00"),
toMs: Date.parse("2026-06-05T00:00:01")
fromMs: parseUtc("2026-05-29T00:00:00"),
toMs: parseUtc("2026-06-05T00:00:01")
});
expect(state.tooLarge).toBe(true);
@@ -30,8 +31,8 @@ describe("tgu2DMapInterval", () => {
const selectedPlan = plan("2026-05-29T01:00:00", "2026-06-07T02:00:00");
const state = getTgu2DMapIntervalState(
{
fromMs: Date.parse("2026-05-29T00:00:00"),
toMs: Date.parse("2026-06-10T00:00:00")
fromMs: parseUtc("2026-05-29T00:00:00"),
toMs: parseUtc("2026-06-10T00:00:00")
},
selectedPlan
);
@@ -1,10 +1,11 @@
import { describe, expect, it } from "vitest";
import type { TguPlanUi, TimelineRow } from "../../model/timelineTypes";
import { buildSequentialLinks, buildTimelineSegments, clipPlanToRange, timeToX } from "./tguTimelineLayout";
import { parseUtc } from "../../utils/utcTime";
const range = {
fromMs: Date.parse("2026-05-29T00:00:00"),
toMs: Date.parse("2026-05-30T00:00:00")
fromMs: parseUtc("2026-05-29T00:00:00"),
toMs: parseUtc("2026-05-30T00:00:00")
};
function plan(id: string, startTime: string, endTime: string, spacecraftId = "56756"): TguPlanUi {
@@ -15,14 +16,14 @@ function plan(id: string, startTime: string, endTime: string, spacecraftId = "56
endTime,
kppId: "KPP-1",
status: "PLANNED",
startMs: Date.parse(startTime),
endMs: Date.parse(endTime)
startMs: parseUtc(startTime),
endMs: parseUtc(endTime)
};
}
describe("tguTimelineLayout", () => {
it("maps time to x inside the selected interval", () => {
expect(timeToX(Date.parse("2026-05-29T12:00:00"), range, 1200)).toBe(600);
expect(timeToX(parseUtc("2026-05-29T12:00:00"), range, 1200)).toBe(600);
});
it("clips partially visible plans to the selected interval", () => {
@@ -30,7 +31,7 @@ describe("tguTimelineLayout", () => {
expect(clipped).toEqual({
startMs: range.fromMs,
endMs: Date.parse("2026-05-29T02:00:00")
endMs: parseUtc("2026-05-29T02:00:00")
});
});