pcp: время — сквозной naive LocalDateTime как локальное настенное время (без UTC)

Отказ от остаточной UTC-семантики: naive LocalDateTime трактуется как
локальное время единой зоны всех сервисов.

Backend (границы внешних систем):
- VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase: ZoneOffset.UTC →
  ZoneId.systemDefault() для проекции внешних Instant/ISO-с-Z в naive.
- тесты границ перевёрнуты на systemDefault (под Europe/Moscow ждут +3).
- чистка UTC-формулировок в DTO-комментариях, мёртвых ссылок на
  docs/standards/DATETIME_UTC.md.

Frontend (pcp-tgu-ops-ui):
- utils/utcTime.ts → utils/localTime.ts: parseLocal/formatLocal*/toLocalIso/
  toLocalInputValue с локальной семантикой (без дописывания Z и timeZone:UTC).
- замена getUTC*/Date.UTC/toISOString-сериализации на локальные аналоги.
- убраны пользовательские подписи «UTC» в редакторе/таймлайне.
- удалён utcTime.guard.test.ts (форсил обратный UTC-инвариант); тест-фикстуры
  сделаны зоно-независимыми. tsc + vitest зелёные под TZ=UTC и TZ=Europe/Moscow.

Включает также прочие незакоммиченные правки рабочего дерева.
This commit is contained in:
Дмитрий Соловьев
2026-06-04 09:50:39 +03:00
parent 5049395911
commit 294b17757d
123 changed files with 556 additions and 535 deletions
@@ -14,7 +14,7 @@ import {
type SurveyRoute
} from "../../api/missionPlanningApi";
import { abandonDraft, createDraft, promoteDraft } from "../../api/tguPlanApi";
import { parseUtc } from "../../utils/utcTime";
import { parseLocal, toLocalIso } from "../../utils/localTime";
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
import { buildStationMarkers } from "../tgu-map-2d/Tgu2DMapLayers";
@@ -150,8 +150,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
.map(([revolution, pts], idx) => ({
index: idx + 1,
revolution,
startMs: parseUtc(pts[0].time),
endMs: parseUtc(pts[pts.length - 1].time),
startMs: parseLocal(pts[0].time),
endMs: parseLocal(pts[pts.length - 1].time),
}));
}, [flightLineData]);
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
@@ -177,7 +177,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
const [shootPasses, setShootPasses] = useState<EditorVisibilityWindow[]>([]);
const [shootPassesLoading, setShootPassesLoading] = useState(false);
const [shootPassesError, setShootPassesError] = useState<string>();
// Окно строящегося маршрута съёмки: задаётся вручную в правой панели (начало/конец, UTC).
// Окно строящегося маршрута съёмки: задаётся вручную в правой панели (начало/конец, локальное).
const [routeWindow, setRouteWindow] = useState<{ startMs: number; endMs: number }>();
// Крен (gamma) строящегося маршрута, градусы. По умолчанию — из прохода, дальше правится вручную.
const [routeRoll, setRouteRoll] = useState<number>();
@@ -275,8 +275,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
useEffect(() => {
const controller = new AbortController();
void fetchRequestsForMap({
endFrom: new Date(window.fromMs).toISOString().slice(0, 19),
beginTo: new Date(window.toMs).toISOString().slice(0, 19)
endFrom: toLocalIso(window.fromMs),
beginTo: toLocalIso(window.toMs)
}).then((result) => {
if (!controller.signal.aborted) {
setRequestItems(result.items);
@@ -365,7 +365,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
void buildSurveyRoute({
satelliteId: numericNorad,
// Окно задано явно (начало/конец) — строим вперёд от начала на выбранную длительность.
time: new Date(routeWindow.startMs).toISOString().slice(0, 19),
time: toLocalIso(routeWindow.startMs),
durationSeconds,
roll: routeRoll,
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
@@ -522,15 +522,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
const draftId = await ensureDraft();
savedRoute = await saveSurveyRoute(draftId, {
satelliteId: numericNorad,
time: new Date(routeWindow.startMs).toISOString().slice(0, 19),
time: toLocalIso(routeWindow.startMs),
durationSeconds: routeDurationSeconds,
roll: routeRoll,
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
revolution: selectedShootPass.revolution ?? 0,
anchor: "FORWARD",
// Окно выбранного плана — чтобы при автосоздании миссии/плана задать корректный интервал.
planStart: new Date(selectedPlan.startMs).toISOString().slice(0, 19),
planEnd: new Date(selectedPlan.endMs).toISOString().slice(0, 19)
planStart: toLocalIso(selectedPlan.startMs),
planEnd: toLocalIso(selectedPlan.endMs)
});
setSurveyRoute(savedRoute);
void refreshDraftModes(draftId);
@@ -614,8 +614,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
zoneStop: selectedZone.onStop.time.slice(0, 19),
surveyModeIds: [...selectedDropSurveyIds],
// Окно выбранного плана — для автосоздания миссии/плана с корректным интервалом.
planStart: new Date(selectedPlan.startMs).toISOString().slice(0, 19),
planEnd: new Date(selectedPlan.endMs).toISOString().slice(0, 19)
planStart: toLocalIso(selectedPlan.startMs),
planEnd: toLocalIso(selectedPlan.endMs)
});
savedDropId = savedDrop.id;
// Перечитываем включения DRAFT (источник истины) и ревалидируем.
@@ -1051,7 +1051,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
}
function visibilityWindowFromParam(param: PointVisibilityParam): EditorVisibilityWindow {
const timeMs = parseUtc(param.time);
const timeMs = parseLocal(param.time);
const half = SHOOT_WINDOW_MS / 2;
return {
startMs: timeMs - half,
@@ -1088,11 +1088,12 @@ function buildStations(selectedPlan: TguEditorTabProps["selectedPlan"]): EditorG
}
/**
* LocalDateTime без зоны от бэка трактуем как UTC (как и поле `time` в ballisticsApi):
* зоны ЗРВ приходят без таймзоны, а времена съёмок — с offset; приводим к одной шкале.
* LocalDateTime без зоны от бэка трактуем как локальное настенное время (как и поле `time`
* в ballisticsApi): зоны ЗРВ приходят без таймзоны, времена съёмок — с offset (его учитываем);
* приводим к одной шкале в локальной зоне браузера.
*/
function localDateTimeMs(s: string): number {
return parseUtc(s);
return parseLocal(s);
}
function buildVisibilityTracks(
@@ -1208,7 +1209,7 @@ function buildEditorMapScene(
const startMs = localDateTimeMs(mode.timeStart);
const endMs = startMs + mode.duration * 1000;
const pts = flightLineData
.map((p) => ({ t: parseUtc(p.time), lat: p.lat, lon: p.long }))
.map((p) => ({ t: parseLocal(p.time), lat: p.lat, lon: p.long }))
.filter((p) => p.t >= startMs && p.t <= endMs)
.sort((a, b) => a.t - b.t)
.map((p) => ({ lat: p.lat, lon: p.lon }));