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:
@@ -7,7 +7,7 @@ import {
|
||||
} from "../../api/tguPlanApi";
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { TguApiError } from "../../model/tguTypes";
|
||||
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
import { buildChainPlanRequest, futureZrvOptions, type ZrvOption } from "./createChainWindows";
|
||||
|
||||
type CreateChainPlanDialogProps = {
|
||||
@@ -123,7 +123,7 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on
|
||||
<option value="">Без истории (новая цепочка с нуля)</option>
|
||||
{baseOptions.map((plan) => (
|
||||
<option key={plan.planId} value={plan.planId}>
|
||||
{formatUtcDateTime(plan.startMs)} · {plan.kppId} · {plan.status}
|
||||
{formatLocalDateTime(plan.startMs)} · {plan.kppId} · {plan.status}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -142,7 +142,7 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on
|
||||
<option value="">— выберите —</option>
|
||||
{zrvOptions.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{formatUtcDateTime(option.startMs)} · {option.kppId}
|
||||
{formatLocalDateTime(option.startMs)} · {option.kppId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -156,7 +156,7 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on
|
||||
.filter((option) => !start || option.endMs > start.startMs)
|
||||
.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{formatUtcDateTime(option.endMs)} · {option.kppId}
|
||||
{formatLocalDateTime(option.endMs)} · {option.kppId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -186,7 +186,7 @@ export function CreateChainPlanDialog({ spacecraftId, plans, onClose, onSent, on
|
||||
) : (
|
||||
result.projectedChain.map((plan) => (
|
||||
<li key={plan.planId}>
|
||||
{formatUtcDateTime(parseUtc(plan.startTime))} – {formatUtcDateTime(parseUtc(plan.endTime))} ·{" "}
|
||||
{formatLocalDateTime(parseLocal(plan.startTime))} – {formatLocalDateTime(parseLocal(plan.endTime))} ·{" "}
|
||||
{plan.kppId}
|
||||
</li>
|
||||
))
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TguPlanDecision } from "../../model/tguTypes";
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { platformLabel } from "./tguTimelineMapper";
|
||||
import { statusStyle } from "./tguStatus";
|
||||
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
||||
|
||||
// Бэкенд (pcp-tgu-service) ждёт решения по плану только waiting-decision-grace-minutes
|
||||
// минут после его startTime; затем IssueDuePlanWorker.tryAdvancePastUnconfirmedPlan
|
||||
@@ -161,7 +161,7 @@ function Detail({ label, value, mono }: { label: string; value?: string; mono?:
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return formatUtcDateTime(parseUtc(value));
|
||||
return formatLocalDateTime(parseLocal(value));
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
|
||||
@@ -27,15 +27,15 @@ import {
|
||||
operationalSpacecraftIds,
|
||||
pausedSpacecraftIds
|
||||
} from "./tguTimelineMapper";
|
||||
import { parseUtc, toUtcInputValue } from "../../utils/utcTime";
|
||||
import { parseLocal, toLocalInputValue } from "../../utils/localTime";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const MINUTE_MS = 60 * 1000;
|
||||
|
||||
export function TguPlanningPage() {
|
||||
const initialRange = useMemo(defaultRange, []);
|
||||
const [fromValue, setFromValue] = useState(toUtcInputValue(initialRange.fromMs));
|
||||
const [toValue, setToValue] = useState(toUtcInputValue(initialRange.toMs));
|
||||
const [fromValue, setFromValue] = useState(toLocalInputValue(initialRange.fromMs));
|
||||
const [toValue, setToValue] = useState(toLocalInputValue(initialRange.toMs));
|
||||
const [appliedRange, setAppliedRange] = useState<TimelineRange>(initialRange);
|
||||
const [rawPlatforms, setRawPlatforms] = useState<TguPlatform[]>([]);
|
||||
const [plans, setPlans] = useState<TguPlanUi[]>([]);
|
||||
@@ -68,8 +68,8 @@ export function TguPlanningPage() {
|
||||
|
||||
const parsedInputRange = useMemo(
|
||||
() => ({
|
||||
fromMs: parseUtc(fromValue),
|
||||
toMs: parseUtc(toValue)
|
||||
fromMs: parseLocal(fromValue),
|
||||
toMs: parseLocal(toValue)
|
||||
}),
|
||||
[fromValue, toValue]
|
||||
);
|
||||
@@ -158,8 +158,8 @@ export function TguPlanningPage() {
|
||||
return {
|
||||
spacecraftId: selectedSpacecraftId,
|
||||
windows: zrvWindows.map((zrv) => ({
|
||||
startMs: parseUtc(zrv.startTime),
|
||||
endMs: parseUtc(zrv.endTime),
|
||||
startMs: parseLocal(zrv.startTime),
|
||||
endMs: parseLocal(zrv.endTime),
|
||||
kppId: zrv.kppId
|
||||
}))
|
||||
};
|
||||
@@ -221,8 +221,8 @@ export function TguPlanningPage() {
|
||||
const quickRange = (hours: number) => {
|
||||
const fromMs = startOfCurrentDayMs();
|
||||
const toMs = fromMs + hours * 60 * 60 * 1000;
|
||||
setFromValue(toUtcInputValue(fromMs));
|
||||
setToValue(toUtcInputValue(toMs));
|
||||
setFromValue(toLocalInputValue(fromMs));
|
||||
setToValue(toLocalInputValue(toMs));
|
||||
setAppliedRange({ fromMs, toMs });
|
||||
setPageError(undefined);
|
||||
setDecisionNotice(undefined);
|
||||
@@ -442,7 +442,7 @@ function defaultRange(): TimelineRange {
|
||||
|
||||
function startOfCurrentDayMs(): number {
|
||||
const now = new Date();
|
||||
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||
}
|
||||
|
||||
function historyDaysForRange(range: TimelineRange): number {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "./tguTimelineLayout";
|
||||
import { platformLabel } from "./tguTimelineMapper";
|
||||
import { statusStyle } from "./tguStatus";
|
||||
import { formatUtcDate, formatUtcDateTime, formatUtcTime, parseUtc } from "../../utils/utcTime";
|
||||
import { formatLocalDate, formatLocalDateTime, formatLocalTime, parseLocal } from "../../utils/localTime";
|
||||
|
||||
export type ZrvWindow = { startMs: number; endMs: number; kppId: string };
|
||||
|
||||
@@ -196,7 +196,7 @@ export function TguTimeline({
|
||||
onSelectZrv(overlapping, Math.max(0, idx));
|
||||
}}
|
||||
>
|
||||
<title>ЗРВ {zrv.kppId} · {formatUtcDateTime(zrv.startMs)} – {formatUtcDateTime(zrv.endMs)}</title>
|
||||
<title>ЗРВ {zrv.kppId} · {formatLocalDateTime(zrv.startMs)} – {formatLocalDateTime(zrv.endMs)}</title>
|
||||
</rect>
|
||||
);
|
||||
})}
|
||||
@@ -268,13 +268,13 @@ function TimelineArrow({ from, to }: { from: TimelinePlanSegment; to: TimelinePl
|
||||
}
|
||||
|
||||
function formatTickDate(ms: number): string {
|
||||
return formatUtcDate(ms);
|
||||
return formatLocalDate(ms);
|
||||
}
|
||||
|
||||
function formatTickTime(ms: number): string {
|
||||
return formatUtcTime(ms);
|
||||
return formatLocalTime(ms);
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return formatUtcDateTime(parseUtc(value));
|
||||
return formatLocalDateTime(parseLocal(value));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ZrvWindow } from "./TguTimeline";
|
||||
import { formatUtcDateTime } from "../../utils/utcTime";
|
||||
import { formatLocalDateTime } from "../../utils/localTime";
|
||||
|
||||
type Props = {
|
||||
windows: ZrvWindow[];
|
||||
@@ -35,7 +35,7 @@ export function TguZrvPanel({ windows, selectedIndex, onSelect, onClose }: Props
|
||||
<span className="tgu-map-request-row__body">
|
||||
<span className="tgu-map-request-row__name">КПП {zrv.kppId}</span>
|
||||
<span className="tgu-map-request-row__meta">
|
||||
{formatUtcDateTime(zrv.startMs)} – {formatUtcDateTime(zrv.endMs)}
|
||||
{formatLocalDateTime(zrv.startMs)} – {formatLocalDateTime(zrv.endMs)}
|
||||
{" · "}
|
||||
{formatDuration(zrv.endMs - zrv.startMs)}
|
||||
</span>
|
||||
@@ -52,10 +52,10 @@ export function TguZrvPanel({ windows, selectedIndex, onSelect, onClose }: Props
|
||||
<dd className="mono">{selected.kppId}</dd>
|
||||
|
||||
<dt>Начало</dt>
|
||||
<dd className="mono">{formatUtcDateTime(selected.startMs)}</dd>
|
||||
<dd className="mono">{formatLocalDateTime(selected.startMs)}</dd>
|
||||
|
||||
<dt>Конец</dt>
|
||||
<dd className="mono">{formatUtcDateTime(selected.endMs)}</dd>
|
||||
<dd className="mono">{formatLocalDateTime(selected.endMs)}</dd>
|
||||
|
||||
<dt>Длительность</dt>
|
||||
<dd className="mono">{formatDuration(selected.endMs - selected.startMs)}</dd>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { buildChainPlanRequest, futureZrvOptions, type ZrvOption } from "./createChainWindows";
|
||||
import type { ZrvWindowResponse } from "../../api/tguPlanApi";
|
||||
|
||||
const NOW = Date.UTC(2026, 5, 3, 12, 0, 0);
|
||||
const NOW = new Date(2026, 5, 3, 12, 0, 0).getTime();
|
||||
|
||||
function zrv(startIso: string, endIso: string, kppId = "KPP-1"): ZrvWindowResponse {
|
||||
return { startTime: startIso, endTime: endIso, kppId };
|
||||
@@ -11,19 +11,19 @@ function zrv(startIso: string, endIso: string, kppId = "KPP-1"): ZrvWindowRespon
|
||||
describe("futureZrvOptions", () => {
|
||||
it("keeps only future zones and sorts by start", () => {
|
||||
const windows = [
|
||||
zrv("2026-06-03T15:00:00Z", "2026-06-03T15:10:00Z"),
|
||||
zrv("2026-06-03T11:00:00Z", "2026-06-03T11:10:00Z"), // в прошлом → отбросить
|
||||
zrv("2026-06-03T13:00:00Z", "2026-06-03T13:10:00Z")
|
||||
zrv("2026-06-03T15:00:00", "2026-06-03T15:10:00"),
|
||||
zrv("2026-06-03T11:00:00", "2026-06-03T11:10:00"), // в прошлом → отбросить
|
||||
zrv("2026-06-03T13:00:00", "2026-06-03T13:10:00")
|
||||
];
|
||||
const options = futureZrvOptions(windows, NOW);
|
||||
expect(options.map((o) => o.startMs)).toEqual([
|
||||
Date.UTC(2026, 5, 3, 13, 0, 0),
|
||||
Date.UTC(2026, 5, 3, 15, 0, 0)
|
||||
new Date(2026, 5, 3, 13, 0, 0).getTime(),
|
||||
new Date(2026, 5, 3, 15, 0, 0).getTime()
|
||||
]);
|
||||
});
|
||||
|
||||
it("produces naive UTC (no zone) start/stop strings", () => {
|
||||
const [option] = futureZrvOptions([zrv("2026-06-03T13:00:00Z", "2026-06-03T13:10:00Z")], NOW);
|
||||
it("produces naive (no zone) start/stop strings", () => {
|
||||
const [option] = futureZrvOptions([zrv("2026-06-03T13:00:00", "2026-06-03T13:10:00")], NOW);
|
||||
expect(option.start).toBe("2026-06-03T13:00:00");
|
||||
expect(option.stop).toBe("2026-06-03T13:10:00");
|
||||
expect(option.kppId).toBe("KPP-1");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal, toLocalIso } from "../../utils/localTime";
|
||||
import type { CreateChainPlanRequest, ZrvWindowResponse } from "../../api/tguPlanApi";
|
||||
|
||||
/**
|
||||
* Опция выбора ЗРВ в диалоге «Создать план». Из стартовой зоны берутся начало окна и КПП, из
|
||||
* конечной — конец окна. `start`/`stop` — naive UTC (ISO без зоны), как ждёт `LocalDateTime` бэка;
|
||||
* конечной — конец окна. `start`/`stop` — naive (ISO без зоны, локальное), как ждёт `LocalDateTime` бэка;
|
||||
* `startMs`/`endMs` — для сортировки/сравнения и подсветки на таймлайне.
|
||||
*/
|
||||
export type ZrvOption = {
|
||||
@@ -11,9 +11,9 @@ export type ZrvOption = {
|
||||
id: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
/** Naive UTC начала зоны. */
|
||||
/** Naive (локальное) начала зоны. */
|
||||
start: string;
|
||||
/** Naive UTC конца зоны. */
|
||||
/** Naive (локальное) конца зоны. */
|
||||
stop: string;
|
||||
kppId: string;
|
||||
};
|
||||
@@ -22,14 +22,14 @@ export type ZrvOption = {
|
||||
export function futureZrvOptions(windows: ZrvWindowResponse[], nowMs: number): ZrvOption[] {
|
||||
return windows
|
||||
.map((zrv) => {
|
||||
const startMs = parseUtc(zrv.startTime);
|
||||
const endMs = parseUtc(zrv.endTime);
|
||||
const startMs = parseLocal(zrv.startTime);
|
||||
const endMs = parseLocal(zrv.endTime);
|
||||
return {
|
||||
id: `${zrv.kppId}|${zrv.startTime}|${zrv.endTime}`,
|
||||
startMs,
|
||||
endMs,
|
||||
start: toNaiveUtc(startMs),
|
||||
stop: toNaiveUtc(endMs),
|
||||
start: toNaive(startMs),
|
||||
stop: toNaive(endMs),
|
||||
kppId: zrv.kppId
|
||||
};
|
||||
})
|
||||
@@ -60,6 +60,6 @@ export function buildChainPlanRequest(
|
||||
};
|
||||
}
|
||||
|
||||
function toNaiveUtc(ms: number): string {
|
||||
return new Date(ms).toISOString().slice(0, 19);
|
||||
function toNaive(ms: number): string {
|
||||
return toLocalIso(ms);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TguPlanUi, TimelineRow } from "../../model/timelineTypes";
|
||||
import { buildSequentialLinks, buildTimelineLayout, clipPlanToRange, timeToX } from "./tguTimelineLayout";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
|
||||
const range = {
|
||||
fromMs: parseUtc("2026-05-29T00:00:00"),
|
||||
toMs: parseUtc("2026-05-30T00:00:00")
|
||||
fromMs: parseLocal("2026-05-29T00:00:00"),
|
||||
toMs: parseLocal("2026-05-30T00:00:00")
|
||||
};
|
||||
|
||||
function plan(id: string, startTime: string, endTime: string, spacecraftId = "56756"): TguPlanUi {
|
||||
@@ -16,14 +16,14 @@ function plan(id: string, startTime: string, endTime: string, spacecraftId = "56
|
||||
endTime,
|
||||
kppId: "KPP-1",
|
||||
status: "PLANNED",
|
||||
startMs: parseUtc(startTime),
|
||||
endMs: parseUtc(endTime)
|
||||
startMs: parseLocal(startTime),
|
||||
endMs: parseLocal(endTime)
|
||||
};
|
||||
}
|
||||
|
||||
describe("tguTimelineLayout", () => {
|
||||
it("maps time to x inside the selected interval", () => {
|
||||
expect(timeToX(parseUtc("2026-05-29T12:00:00"), range, 1200)).toBe(600);
|
||||
expect(timeToX(parseLocal("2026-05-29T12:00:00"), range, 1200)).toBe(600);
|
||||
});
|
||||
|
||||
it("clips partially visible plans to the selected interval", () => {
|
||||
@@ -31,7 +31,7 @@ describe("tguTimelineLayout", () => {
|
||||
|
||||
expect(clipped).toEqual({
|
||||
startMs: range.fromMs,
|
||||
endMs: parseUtc("2026-05-29T02:00:00")
|
||||
endMs: parseLocal("2026-05-29T02:00:00")
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import type { TguPlatform, TguPlanStatus } from "../../model/tguTypes";
|
||||
import type { TguPlanUi } from "../../model/timelineTypes";
|
||||
import { isOperationalPlatform, operationalSpacecraftIds, pausedSpacecraftIds } from "./tguTimelineMapper";
|
||||
import { toLocalIso } from "../../utils/localTime";
|
||||
|
||||
function platform(overrides: Partial<TguPlatform>): TguPlatform {
|
||||
return { id: "x", noradId: 1, status: "OPERATIONAL", ...overrides };
|
||||
@@ -11,8 +12,8 @@ function uiPlan(spacecraftId: string, status: TguPlanStatus, startMs: number): T
|
||||
return {
|
||||
planId: `${spacecraftId}-${status}-${startMs}`,
|
||||
spacecraftId,
|
||||
startTime: new Date(startMs).toISOString().slice(0, 19),
|
||||
endTime: new Date(startMs + 3_600_000).toISOString().slice(0, 19),
|
||||
startTime: toLocalIso(startMs),
|
||||
endTime: toLocalIso(startMs + 3_600_000),
|
||||
kppId: "KPP-1",
|
||||
status,
|
||||
startMs,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TguPlan, TguPlatform } from "../../model/tguTypes";
|
||||
import type { TguPlanUi, TguPlatformUi, TimelineRange, TimelineRow } from "../../model/timelineTypes";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { parseLocal } from "../../utils/localTime";
|
||||
|
||||
const PROBLEM_STATUSES = new Set(["REJECTED", "START_AMBIGUOUS"]);
|
||||
|
||||
@@ -27,8 +27,8 @@ export function mapPlans(plans: TguPlan[], platforms: TguPlatform[]): TguPlanUi[
|
||||
|
||||
return plans
|
||||
.map((plan) => {
|
||||
const startMs = parseUtc(plan.startTime);
|
||||
const endMs = parseUtc(plan.endTime);
|
||||
const startMs = parseLocal(plan.startTime);
|
||||
const endMs = parseLocal(plan.endTime);
|
||||
const platform = platformByNorad.get(plan.spacecraftId);
|
||||
return {
|
||||
...plan,
|
||||
|
||||
Reference in New Issue
Block a user