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:
@@ -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 }));
|
||||
|
||||
Reference in New Issue
Block a user