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
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <old-ip> <new-ip>"
echo "Example: $0 192.168.60.201 192.168.60.201"
exit 1
fi
OLD_IP="$1"
NEW_IP="$2"
is_valid_ip() {
local ip="$1"
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
IFS='.' read -r a b c d <<< "$ip"
for part in "$a" "$b" "$c" "$d"; do
(( part >= 0 && part <= 255 )) || return 1
done
}
if ! is_valid_ip "$OLD_IP"; then
echo "Invalid old IP: $OLD_IP"
exit 1
fi
if ! is_valid_ip "$NEW_IP"; then
echo "Invalid new IP: $NEW_IP"
exit 1
fi
export OLD_IP
export NEW_IP
changed_files=0
skipped_files=0
while IFS= read -r -d '' file; do
# Пропускаем бинарные файлы
if ! grep -Iq . "$file"; then
skipped_files=$((skipped_files + 1))
continue
fi
if perl -0ne '
BEGIN { $old = quotemeta($ENV{"OLD_IP"}); }
exit 0 if /(?<![0-9.])$old(?![0-9.])/;
exit 1;
' "$file"; then
perl -0pi -e '
BEGIN {
$old = quotemeta($ENV{"OLD_IP"});
$new = $ENV{"NEW_IP"};
}
s/(?<![0-9.])$old(?![0-9.])/$new/g;
' "$file"
echo "changed: $file"
changed_files=$((changed_files + 1))
fi
done < <(
find . \
-path './.git' -prune -o \
-type f \
-print0
)
echo ""
echo "Done."
echo "IP replaced: $OLD_IP -> $NEW_IP"
echo "Files updated: $changed_files"
echo "Files skipped: $skipped_files"
+4 -4
View File
@@ -1,16 +1,16 @@
# TODO before merge request: вернуть старые адреса окружения.
# pcp.infra.postgres.host: 192.168.1.8
# pcp.infra.postgres.host: 192.168.60.201
pcp:
infra:
postgres:
host: 192.168.1.8
host: 192.168.60.201
port: 35400
kafka:
host: 192.168.1.8
host: 192.168.60.201
port: 19092
network:
host: 192.168.1.8
host: 192.168.60.201
ports:
config: 8888
+1 -1
View File
@@ -3,7 +3,7 @@ set -euo pipefail
# PostgreSQL connection settings. Override via env if needed:
# PGHOST=... PGPORT=... PGUSER=... PGPASSWORD=... ./create_pcp_databases.sh
PGHOST="${PGHOST:-192.168.1.8}"
PGHOST="${PGHOST:-192.168.60.201}"
PGPORT="${PGPORT:-5432}"
PGUSER="${PGUSER:-postgres}"
PGPASSWORD="${PGPASSWORD:-password}"
+1 -1
View File
@@ -21,7 +21,7 @@
- существующие публичные контракты не сломаны без явного запроса;
- добавлены или обновлены релевантные тесты;
- не внесено несвязанных правок;
- время на API — naive `LocalDateTime` без зоны; backend штампует `now` через `LocalDateTime.now(ZoneOffset.UTC)`, фронт парсит/форматирует через `utils/utcTime` без `new Date("<строка>")`;
- время на API — naive `LocalDateTime` без зоны; backend штампует `now` через `LocalDateTime.now()`, фронт парсит/форматирует через `utils/utcTime` без `new Date("<строка>")`;
- egress фронта к бэку — naive ISO (`YYYY-MM-DDTHH:mm:ss`, без Z);
- validation выполнен по правилам `AGENTS.md`, либо пропуски явно объяснены.
+30
View File
@@ -0,0 +1,30 @@
# Следующая задача: маршруты, сбросы и перенос включений между планами
> Статус: запланировано (дизайн ещё не начат). Это фиксация намерения, не финальный план.
Следующий блок работ — три связанные темы вокруг содержимого плана (включений) и
их съёмочно-сбросовой обвязки:
## 1. Построение маршрутов
Довести построение маршрутов наблюдения по плану. Сейчас маршруты порождаются в
потоке `createSatelliteMission` (BPMN), где `planId == missionId`; результат
ложится в БД mission-planing. Уточнить: когда и по какому триггеру строятся
маршруты для нового/перевыданного плана, и как они привязаны к включениям.
## 2. Формирование сбросов
Формирование сбросов (`DropMode`) поверх съёмочных включений (`SurveyMode`):
расчёт окон сброса, привязка к ЗРВ/КПП, согласование времени сброса со временем
съёмки. Внутренние времена — naive `LocalDateTime` (см. сквозную модель без зон).
## 3. Перенос включений из плана в план
При перевыдаче/версионировании плана переносить уже сформированные включения
(съёмки + сбросы) из предыдущего плана в новый — чтобы не терять набранное
содержимое цепочки. Определить, что переносится как есть, что пересчитывается,
и как это стыкуется с двумя осями версионирования (цепочка ТГУ + контент миссии).
## Контекст / связанные места
- Маршруты и их сохранение: `createSatelliteMission`, `planId == missionId`.
- Сбросы/съёмки: `pcp-mission-planing-service``SurveyMode`/`DropMode`,
`MissionPlaningService`.
- Версионирование/перевыдача плана — две оси (цепочка ТГУ + контент миссии).
- Время — сквозной naive `LocalDateTime` без зоны (после отката UTC-слоя).
@@ -0,0 +1,13 @@
1. Перевод сервиса станций на ordinis по примеру получения аппаратов в сервисе tgu
2. Ошибка при попытке принять план во вкладке планы
3. Происходит накопление целей съемки, этого делать не нужно, пусть будет просто перекрестие на карте
А на панели справа пусть будет написано опорная точка прицеливания и ее координаты
4. Дата время в формате 24 часов а не AM PM, во всем интерфейсе.
5. Добавить маску важности, чтобы можно было легко ввести 0.1 или 0.3
6. При нажатии на включение нужно центрировать экран на него и подсвечивать выбор, будто кликнули по самому объекту на карте
7. Сбросы нужно отобрадать на картооснове как толстую яркую линию на трассе, время начала конца линии - время начала конца сброса
8. При нажатии на ЗРВ в окне Планы нужно справа отображать информацию по этим ЗРВ и если их несколько то по нескольких по примеру завок на вкладке Карта
10. Проблема того, что в одном и том же месте есть план Rejected и план WAIting_decision (скриншот сохранен)
@@ -2,8 +2,9 @@ package space.nstart.pcp.pcp_types_lib.message
import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import space.nstart.pcp.pcp_types_lib.utils.json.LocalDateTimeTimeZoneSerializer
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.*
@@ -19,10 +20,9 @@ class KafkaMessage<out T> (
val id = UUID.randomUUID().toString()
var source = "Example"
// Время конверта — naive LocalDateTime, штампуется по настенным часам UTC (now(ZoneOffset.UTC)),
// сериализуется как ISO без зоны.
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
val time: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]")
@JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class)
val time: LocalDateTime = LocalDateTime.now()
val dataContentType = "application/json"
val specVersion = "1.0"
@@ -0,0 +1,22 @@
package space.nstart.pcp.pcp_types_lib.utils.json
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class LocalDateTimeTimeZoneSerializer: JsonSerializer<LocalDateTime?>() {
companion object {
val fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSxxx")
}
override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) {
//todo: проверить корректность зоны по нормальным правилам
// p1.writeString(p0?.atZone(ZoneId.of("Europe/Moscow"))?.format(fmt))
p1.writeString(p0?.atZone(ZoneId.systemDefault())?.format(fmt))
}
}
@@ -13,7 +13,6 @@ import jakarta.persistence.Table
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.KeplersDTO
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.PI
@@ -27,7 +26,7 @@ class AscNodeEntity(
@Column(nullable = false)
val satelliteId : Long = 0,
@Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val time : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revolution : Long = 0,
val longitude : Double = 0.0,
@@ -10,7 +10,6 @@ import jakarta.persistence.Table
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.PI
@Entity
@@ -27,13 +26,13 @@ class EarthCellViewsEntity(
@Column(name = "rev_begin", nullable = false)
val revolutionBegin : Long = 0,
@Column(nullable = false)
val timeBegin : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val timeBegin : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revSignBegin : Int = 0,
@Column(name = "rev_end", nullable = false)
val revolutionEnd: Long = 0,
@Column(nullable = false)
val timeEnd : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val timeEnd : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revSignEnd : Int = 0,
@Column(name = "roll_min")
@@ -9,7 +9,6 @@ import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
@Entity
@Table(name = "earth_coverage")
@@ -23,9 +22,9 @@ class EarthCoverageEntity(
@Column(nullable = false)
val cellNumber : Long = 0,
@Column(nullable = false)
val tStart : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val tStart : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val tStop : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val tStop : LocalDateTime = LocalDateTime.now(),
) {
constructor(fl : FleghtLineSector, sector : Int, sat : Long) : this(
null,
@@ -6,7 +6,6 @@ import jakarta.persistence.*
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.PI
@@ -20,7 +19,7 @@ class FlightLineEntity(
@Column(nullable = false)
val satelliteId : Long = 0,
@Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val time : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revolution : Long = 0,
@Column(nullable = false)
@@ -7,7 +7,6 @@ import ballistics.utils.toDateTime
import jakarta.persistence.*
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import java.time.LocalDateTime
import java.time.ZoneOffset
@Entity
@Table(name = "pdcm")
@@ -19,7 +18,7 @@ class PDCMEntity (
@Column(nullable = false)
val satelliteId : Long = 0,
@Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val time : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revolution : Long = 0,
val x: Double = 0.0,
@@ -13,7 +13,6 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import java.time.LocalDateTime
import java.time.ZoneOffset
@Entity
@Table(name = "satellite_initial_conditions")
@@ -28,7 +27,7 @@ class SatelliteInitialConditionEntity(
@Column(name = "movement_model", nullable = false)
val movementModel: MovementModel = MovementModel.FOTO,
@Column(name = "orb_time", nullable = false)
val orbTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val orbTime: LocalDateTime = LocalDateTime.now(),
@Column(name = "orb_revolution", nullable = false)
val orbRevolution: Long = 0,
@Column(nullable = false)
@@ -48,7 +47,7 @@ class SatelliteInitialConditionEntity(
@Column(nullable = false)
val f81: Double = 147.8,
@Column(name = "received_at", nullable = false)
val receivedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val receivedAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "calculated_at")
var calculatedAt: LocalDateTime? = null,
@Column(name = "last_calculated", nullable = false)
@@ -69,7 +68,7 @@ class SatelliteInitialConditionEntity(
f81 = request.ic.f81
)
fun markLastCalculated(calculatedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)) {
fun markLastCalculated(calculatedAt: LocalDateTime = LocalDateTime.now()) {
this.calculatedAt = calculatedAt
this.lastCalculated = true
}
@@ -63,7 +63,6 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.collections.forEach
import kotlin.math.PI
import kotlin.math.abs
@@ -762,8 +761,8 @@ class SatelliteService {
fun getOrbPoints(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) =
pdcmRepository.findBySatelliteIdAndTimeBetween(
id,
tn?: LocalDateTime.now(ZoneOffset.UTC),
tk?: LocalDateTime.now(ZoneOffset.UTC).plusDays(1))
tn?: LocalDateTime.now(),
tk?: LocalDateTime.now().plusDays(1))
.map { it.toDTO()
}
@@ -776,8 +775,8 @@ class SatelliteService {
fun getFL(id : Long, tn : LocalDateTime?, tk : LocalDateTime?, step : Double?) =
flightLineRepository.findBySatelliteIdAndTimeBetween(id,
tn?: LocalDateTime.now(ZoneOffset.UTC),
tk?: LocalDateTime.now(ZoneOffset.UTC).plusDays(1)
tn?: LocalDateTime.now(),
tk?: LocalDateTime.now().plusDays(1)
)
.map { it.toDTO() }
@@ -898,8 +897,8 @@ class SatelliteService {
return ascNodeRepository.findBySatelliteIdAndTimeBetween(
id,
tn ?: LocalDateTime.now(ZoneOffset.UTC),
tk ?: LocalDateTime.now(ZoneOffset.UTC).plusDays(1)
tn ?: LocalDateTime.now(),
tk ?: LocalDateTime.now().plusDays(1)
)
.map {
val keps = calculateKeplerParams(
@@ -13,7 +13,6 @@ import jakarta.persistence.Index
import jakarta.persistence.OneToMany
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
@Entity
@Table(
@@ -35,7 +34,7 @@ class ComplexPlanRunEntity(
var status: ComplexPlanRunStatus = ComplexPlanRunStatus.CREATED,
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "started_at")
var startedAt: LocalDateTime? = null,
@@ -47,10 +46,10 @@ class ComplexPlanRunEntity(
var durationMs: Long? = null,
@Column(name = "interval_start", nullable = false)
var intervalStart: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var intervalStart: LocalDateTime = LocalDateTime.now(),
@Column(name = "interval_end", nullable = false)
var intervalEnd: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var intervalEnd: LocalDateTime = LocalDateTime.now(),
@Column(name = "requested_by")
var requestedBy: String? = null,
@@ -15,7 +15,6 @@ import jakarta.persistence.ManyToOne
import jakarta.persistence.Table
import jakarta.persistence.Index
import java.time.LocalDateTime
import java.time.ZoneOffset
@Entity
@Table(
@@ -44,10 +43,10 @@ class SatelliteModeEntity(
var type: SatelliteModeType = SatelliteModeType.SURVEY,
@Column(name = "start_time", nullable = false)
var startTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var startTime: LocalDateTime = LocalDateTime.now(),
@Column(name = "end_time", nullable = false)
var endTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var endTime: LocalDateTime = LocalDateTime.now(),
@Column(name = "revolution", nullable = false)
var revolution: Long = 0,
@@ -13,7 +13,6 @@ import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToMany
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
@Entity
@Table(
@@ -35,13 +34,13 @@ class SatelliteModeSnapshotEntity(
var satelliteId: Long = 0,
@Column(name = "interval_start", nullable = false)
var intervalStart: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var intervalStart: LocalDateTime = LocalDateTime.now(),
@Column(name = "interval_end", nullable = false)
var intervalEnd: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var intervalEnd: LocalDateTime = LocalDateTime.now(),
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "is_active", nullable = false)
var isActive: Boolean = false,
@@ -3,12 +3,11 @@ package space.nstart.pcp.pcp_satellites_service.model.mode
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
import space.nstart.pcp.pcp_types_lib.dto.satellite.modes.SurveyModeInfoDTO
import java.time.LocalDateTime
import java.time.ZoneOffset
class SurveyMode(
val revolution : Long = 1,
val time : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val timeStop : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
val time : LocalDateTime = LocalDateTime.now(),
val timeStop : LocalDateTime = LocalDateTime.now(),
val roll : Double = 0.0,
val latitude : Double = 0.0,
val longitude : Double = 0.0,
@@ -17,7 +17,6 @@ import space.nstart.pcp.pcp_satellites_service.service.ComplexMissionService
import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunOrchestrationService
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -48,7 +48,6 @@ import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
@@ -842,7 +841,7 @@ class BasicPlanCalculator(
val resultFilePath = if (request.saveResultToFile && status == ComplexPlanCalculationStatus.COMPLETED) {
observationParametersDebugFileWriter.write(
ObservationParametersDebugFileDTO(
createdAt = LocalDateTime.now(ZoneOffset.UTC),
createdAt = LocalDateTime.now(),
requestId = request.requestId,
satelliteIds = selectedSatelliteIds,
missingSatelliteIds = missingSatelliteIds,
@@ -10,7 +10,6 @@ import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
@Table(name = "dynamic_plan_route")
@@ -32,10 +31,10 @@ class DynamicPlanRouteEntity(
var satelliteId: Long = 0,
@Column(name = "start_time", nullable = false)
var startTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var startTime: LocalDateTime = LocalDateTime.now(),
@Column(name = "end_time", nullable = false)
var endTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var endTime: LocalDateTime = LocalDateTime.now(),
@Column(name = "duration", nullable = false)
var duration: Double = 0.0,
@@ -10,7 +10,6 @@ import space.nstart.pcp.complan.types.DynamicPlanCalculationMode
import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
@Table(name = "dynamic_plan_run")
@@ -48,7 +47,7 @@ class DynamicPlanRunEntity(
var filterCoveredRoutes: Boolean = false,
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "started_at")
var startedAt: LocalDateTime? = null,
@@ -25,7 +25,6 @@ import space.nstart.pcp.complan.types.DynamicPlanRunStatus
import space.nstart.pcp.complan.types.IntervalItemDTO
import space.nstart.pcp.complan.types.SurveyContour
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
import java.util.concurrent.ExecutorService
import kotlin.math.roundToLong
@@ -60,7 +59,7 @@ class DynamicPlanRunService(
calculationMode = request.calculationMode,
revolutionMode = request.revolutionMode,
filterCoveredRoutes = request.filterCoveredRoutes,
createdAt = LocalDateTime.now(ZoneOffset.UTC)
createdAt = LocalDateTime.now()
)
)
} ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create calculation run")
@@ -136,7 +135,7 @@ class DynamicPlanRunService(
return@execute 0
}
val finishedAt = LocalDateTime.now(ZoneOffset.UTC)
val finishedAt = LocalDateTime.now()
runs.forEach { run ->
run.status = DynamicPlanRunStatus.FAILED
run.finishedAt = finishedAt
@@ -172,7 +171,7 @@ class DynamicPlanRunService(
transactionTemplate.execute {
val run = runRepository.findById(runId).orElse(null) ?: return@execute null
run.status = DynamicPlanRunStatus.RUNNING
run.startedAt = LocalDateTime.now(ZoneOffset.UTC)
run.startedAt = LocalDateTime.now()
run.errorMessage = null
runRepository.save(run)
run.toRequest()
@@ -183,7 +182,7 @@ class DynamicPlanRunService(
val run = runRepository.findById(runId).orElseThrow()
val result = output.result
run.status = DynamicPlanRunStatus.COMPLETED
run.finishedAt = LocalDateTime.now(ZoneOffset.UTC)
run.finishedAt = LocalDateTime.now()
run.durationMs = result.durationMs
run.resultStatus = result.status.name
run.routesCount = result.routesCount
@@ -201,7 +200,7 @@ class DynamicPlanRunService(
transactionTemplate.execute {
val run = runRepository.findById(runId).orElseThrow()
run.status = DynamicPlanRunStatus.FAILED
run.finishedAt = LocalDateTime.now(ZoneOffset.UTC)
run.finishedAt = LocalDateTime.now()
run.errorMessage = error.message ?: error::class.java.simpleName
runRepository.save(run)
}
@@ -6,7 +6,6 @@ import org.locationtech.jts.geom.LineString
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Класс космического аппарата**
@@ -18,9 +17,9 @@ class SpaceCraft (
val flightLine : MutableList<FlightLineDTO> = mutableListOf()
/**Время начала планирования аппарата*/
var timeStart: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var timeStart: LocalDateTime = LocalDateTime.now()
/**Время конца планирования аппарата*/
var timeStop : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var timeStop : LocalDateTime = LocalDateTime.now()
/**
* Создание линий полета восходящего и нисходящего витка
* @param geomFactory фабрика геометрии
@@ -12,9 +12,9 @@ object CloneMode {
data class MissionCloneRequest(
/** [CloneMode.REPLACE] — полный клон; [CloneMode.FORK_FORWARD] — только включения, перекрывающиеся с окном. */
val mode: String,
/** Начало окна нового плана (UTC naive). Обязательно для fork-forward. */
/** Начало окна нового плана (naive, локальное время). Обязательно для fork-forward. */
val windowStart: LocalDateTime? = null,
/** Конец окна нового плана (UTC naive). Обязательно для fork-forward. */
/** Конец окна нового плана (naive, локальное время). Обязательно для fork-forward. */
val windowStop: LocalDateTime? = null
)
@@ -24,8 +24,8 @@ data class MissionCloneResponse(val newMissionId: UUID)
data class MissionBlankRequest(
/** КА (= noradId). */
val satelliteId: Long,
/** Начало окна миссии (UTC naive). */
/** Начало окна миссии (naive, локальное время). */
val missionStart: LocalDateTime,
/** Конец окна миссии (UTC naive). */
/** Конец окна миссии (naive, локальное время). */
val missionStop: LocalDateTime
)
@@ -15,7 +15,6 @@ import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
@Component
@@ -60,7 +59,7 @@ class BookedSlotsEventPublisher(
val event = BookedSlotsStatusChangedEvent(
bookedSlotIds = normalizedIds,
status = status,
occurredAt = LocalDateTime.now(ZoneOffset.UTC),
occurredAt = LocalDateTime.now(),
sourceService = applicationName,
missionId = missionId,
modeId = modeId,
@@ -62,7 +62,6 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
import kotlin.math.PI
import kotlin.math.abs
@@ -433,7 +432,7 @@ class MissionPlaningService(
overrideIntervalEnd: LocalDateTime? = null,
comPlanSnapshotId: Long? = null
): MissionSurveyCalculationResponseDTO {
val startedAt = LocalDateTime.now(ZoneOffset.UTC)
val startedAt = LocalDateTime.now()
logger.info(
"Расчет плана съемки stage 5 по persisted snapshot этапа 4 для миссии {}: requestedComPlanSnapshotId={}",
missionId,
@@ -521,7 +520,7 @@ class MissionPlaningService(
)
if (survs.isEmpty()) {
val finishedAt = LocalDateTime.now(ZoneOffset.UTC)
val finishedAt = LocalDateTime.now()
return MissionSurveyCalculationResponseDTO(
missionId = missionId,
completed = true,
@@ -566,7 +565,7 @@ class MissionPlaningService(
modeBookings.size
)
val finishedAt = LocalDateTime.now(ZoneOffset.UTC)
val finishedAt = LocalDateTime.now()
return MissionSurveyCalculationResponseDTO(
missionId = missionId,
completed = true,
@@ -8,7 +8,6 @@ import org.nstart.dep265.requestservice.repository.RequestRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneOffset
class GridRebuildAlreadyRunningException : RuntimeException("Grid rebuild is already running")
@@ -41,7 +40,7 @@ class GridRebuildService(
return GridRebuildResponseDto(
cellsCount = earthCellRepository.count(),
rebuiltRequestProjectionsCount = activeRequests.size.toLong(),
rebuiltAt = LocalDateTime.now(ZoneOffset.UTC),
rebuiltAt = LocalDateTime.now(),
)
}
@@ -12,7 +12,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.concurrent.ExecutionException
@Service
@@ -70,7 +69,7 @@ class OutboxPublisherService(
.get()
event.status = OutboxEventStatus.PUBLISHED
event.publishedAt = LocalDateTime.now(ZoneOffset.UTC)
event.publishedAt = LocalDateTime.now()
event.errorMessage = null
outboxEventRepository.save(event)
@@ -39,7 +39,6 @@ import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
class InvalidRequestListSortException(message: String) : IllegalArgumentException(message)
@@ -65,7 +64,7 @@ class RequestService(
"beginDateTime должен быть раньше endDateTime"
}
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val normalizedGeometry = geometryService.parsePolygonalGeometry(request.geometry)
val normalizedGeometryWkt = geometryService.toWkt(normalizedGeometry)
val geometryArea = geometryService.area(normalizedGeometry)
@@ -153,7 +152,7 @@ class RequestService(
@Transactional
fun deleteRequest(id: UUID): DeleteRequestResponseDto? {
val request = requestRepository.findById(id).orElse(null) ?: return null
val deletedAt = request.deletedAt ?: LocalDateTime.now(ZoneOffset.UTC)
val deletedAt = request.deletedAt ?: LocalDateTime.now()
request.status = RequestStatus.DELETED
request.deletedAt = deletedAt
request.updatedAt = deletedAt
@@ -15,7 +15,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
import kotlin.math.max
import kotlin.math.min
@@ -44,7 +43,7 @@ class RouteMatchingService(
val routeId = route.routeId
val routeRollRange = route.rollAngle
val matchedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
val matchedAt: LocalDateTime = LocalDateTime.now()
val routeGeometry = geometryService.parsePolygonalGeometry(route.geometry)
val candidateRequestIds = requestRepository.findRequestIdsForRoute(
@@ -13,7 +13,6 @@ import org.nstart.dep265.requestservice.repository.EarthCellRepository
import org.nstart.dep265.requestservice.repository.RequestCellRepository
import org.nstart.dep265.requestservice.repository.RequestRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -113,14 +112,14 @@ class RequestGridProjectionServiceTest {
geometryArea = 1.0,
remainingArea = 1.0,
importance = importance,
beginDateTime = LocalDateTime.now(ZoneOffset.UTC),
endDateTime = LocalDateTime.now(ZoneOffset.UTC).plusDays(1),
beginDateTime = LocalDateTime.now(),
endDateTime = LocalDateTime.now().plusDays(1),
highPriorityTransmit = false,
surveyType = RequestSurveyType.OPTICS,
status = RequestStatus.ACTIVE,
matchCount = 0,
createdAt = LocalDateTime.now(ZoneOffset.UTC),
updatedAt = LocalDateTime.now(ZoneOffset.UTC),
createdAt = LocalDateTime.now(),
updatedAt = LocalDateTime.now(),
)
)
@@ -29,7 +29,6 @@ import org.springframework.transaction.support.SimpleTransactionStatus
import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.Optional
import java.util.UUID
@@ -48,7 +47,7 @@ class RouteMatchingServiceTest {
geometryService = geometryService,
)
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
val intervalEnd = intervalBegin.plusHours(1)
val request = request(
beginDateTime = intervalBegin,
@@ -97,7 +96,7 @@ class RouteMatchingServiceTest {
geometryService = geometryService,
)
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
val intervalEnd = intervalBegin.plusHours(1)
val originalRemainingGeometry = "POLYGON((1 1, 2 1, 2 2, 1 2, 1 1))"
val request = request(
@@ -147,7 +146,7 @@ class RouteMatchingServiceTest {
geometryService = geometryService,
)
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
val request = request(
beginDateTime = intervalBegin,
endDateTime = intervalBegin.plusHours(1),
@@ -183,7 +182,7 @@ class RouteMatchingServiceTest {
geometryService = geometryService,
)
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
val request = request(
beginDateTime = intervalBegin,
endDateTime = intervalBegin.plusHours(1),
@@ -231,7 +230,7 @@ class RouteMatchingServiceTest {
geometryService = geometryService,
)
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
val request = request(
beginDateTime = intervalBegin,
endDateTime = intervalBegin.plusHours(1),
@@ -269,7 +268,7 @@ class RouteMatchingServiceTest {
geometryService = geometryService,
)
val intervalBegin = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(5).withNano(0)
val intervalBegin = LocalDateTime.now().minusMinutes(5).withNano(0)
val request = request(
beginDateTime = intervalBegin,
endDateTime = intervalBegin.plusHours(1),
@@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper as JpaJsonObjectMapper
import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -39,7 +38,7 @@ class RoutePassportIngestionService(
rawPayload: String,
sourceRecord: RoutePassportSourceRecordMetadata,
) {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val routePassportDto = parsedMessage.routePassportDto
val existingRoutePassport = routePassportRepository.findById(routePassportDto.routeId)
val routeCreated = existingRoutePassport.isEmpty
@@ -9,7 +9,6 @@ import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxStatus
import space.nstart.pcp.routepassportconsumer.repository.RoutePassportOutboxRepository
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -33,7 +32,7 @@ class RoutePassportOutboxService(
return emptyList()
}
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val lockedUntil = now.plus(Duration.ofMillis(outboxProperties.lockTimeoutMs))
val claimedRows = routePassportOutboxRepository.findDueForUpdateSkipLocked(outboxProperties.batchSize)
@@ -71,7 +70,7 @@ class RoutePassportOutboxService(
*/
@Transactional
fun markPublished(outboxId: UUID) {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val outbox = findOutbox(outboxId)
outbox.status = RoutePassportOutboxStatus.PUBLISHED
@@ -100,7 +99,7 @@ class RoutePassportOutboxService(
*/
@Transactional
fun markRetryOrFailed(outboxId: UUID, exception: Exception) {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val outbox = findOutbox(outboxId)
outbox.attempts += 1
@@ -98,7 +98,7 @@
],
"assets": {
"schema-calibration-vh": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/calibration-s1a-iw-grd-vh-20250625t013854-20250625t013919-059796-076d15-002.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120818Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=26988c6a3490f71382ae9e61ff0d7b59aca32ec652993a785d24e8dd81e70939",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/calibration-s1a-iw-grd-vh-20250625t013854-20250625t013919-059796-076d15-002.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120818Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=26988c6a3490f71382ae9e61ff0d7b59aca32ec652993a785d24e8dd81e70939",
"title": "xml",
"description": null,
"type": "application/xml",
@@ -107,7 +107,7 @@
]
},
"schema-calibration-vv": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/calibration-s1a-iw-grd-vv-20250625t013854-20250625t013919-059796-076d15-001.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120818Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=edd6957a0404ead07e7a31d0759db26b3a6ad33394247c177dc41a7abf5972d0",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/calibration-s1a-iw-grd-vv-20250625t013854-20250625t013919-059796-076d15-001.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120818Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=edd6957a0404ead07e7a31d0759db26b3a6ad33394247c177dc41a7abf5972d0",
"title": "xml",
"description": null,
"type": "application/xml",
@@ -116,7 +116,7 @@
]
},
"schema-noise-vh": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/noise-s1a-iw-grd-vh-20250625t013854-20250625t013919-059796-076d15-002.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120818Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=3efe46114ed6dfeac898963510d3ba9c8b537c803b1fa69a5a03d5b462a1665c",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/noise-s1a-iw-grd-vh-20250625t013854-20250625t013919-059796-076d15-002.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120818Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=3efe46114ed6dfeac898963510d3ba9c8b537c803b1fa69a5a03d5b462a1665c",
"title": "xml",
"description": null,
"type": "application/xml",
@@ -125,7 +125,7 @@
]
},
"schema-noise-vv": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/noise-s1a-iw-grd-vv-20250625t013854-20250625t013919-059796-076d15-001.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120818Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=0ca91479ba4132dbf13c2637f97567eb29ee9ca818009eaf501072995fe784ca",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/noise-s1a-iw-grd-vv-20250625t013854-20250625t013919-059796-076d15-001.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120818Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=0ca91479ba4132dbf13c2637f97567eb29ee9ca818009eaf501072995fe784ca",
"title": "xml",
"description": null,
"type": "application/xml",
@@ -134,7 +134,7 @@
]
},
"schema-product-vh": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/s1a-iw-grd-vh-20250625t013854-20250625t013919-059796-076d15-002.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120819Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=39817f0b7610c0b20a23d0fae24e7183f0d3719ba6ab2f315c320c9c6530274f",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/s1a-iw-grd-vh-20250625t013854-20250625t013919-059796-076d15-002.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120819Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=39817f0b7610c0b20a23d0fae24e7183f0d3719ba6ab2f315c320c9c6530274f",
"title": "xml",
"description": null,
"type": "application/xml",
@@ -143,7 +143,7 @@
]
},
"schema-product-vv": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/s1a-iw-grd-vv-20250625t013854-20250625t013919-059796-076d15-001.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120819Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=85d28bc2bb94724b4db7df84121d4252b423fa429204e4b25e06b9d725f3aeee",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/s1a-iw-grd-vv-20250625t013854-20250625t013919-059796-076d15-001.xml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120819Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=85d28bc2bb94724b4db7df84121d4252b423fa429204e4b25e06b9d725f3aeee",
"title": "xml",
"description": null,
"type": "application/xml",
@@ -152,7 +152,7 @@
]
},
"safe-manifest": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/manifest.safe?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120819Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=dbb22d57d031a412e2b46273edf32cfcb7fbdae1fa1fcc74bdcaa39a5e086f53",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/manifest.safe?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120819Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=dbb22d57d031a412e2b46273edf32cfcb7fbdae1fa1fcc74bdcaa39a5e086f53",
"title": "safe",
"description": null,
"type": "application/xml",
@@ -161,7 +161,7 @@
]
},
"vh": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/s1a-iw-grd-vh-20250625t013854-20250625t013919-059796-076d15-002_cog.tif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120824Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=dbecc9e04e976751e855f53e9a9a8db5d6365a30ac7ec116f31b0936ee375648",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/s1a-iw-grd-vh-20250625t013854-20250625t013919-059796-076d15-002_cog.tif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120824Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=dbecc9e04e976751e855f53e9a9a8db5d6365a30ac7ec116f31b0936ee375648",
"title": "tif",
"description": null,
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
@@ -170,7 +170,7 @@
]
},
"vv": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/s1a-iw-grd-vv-20250625t013854-20250625t013919-059796-076d15-001_cog.tif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120829Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=34cf1e5f5174b7cb5ca5d173ff699923819d5df28486436d972efa59ac9b1ddb",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/s1a-iw-grd-vv-20250625t013854-20250625t013919-059796-076d15-001_cog.tif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120829Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=34cf1e5f5174b7cb5ca5d173ff699923819d5df28486436d972efa59ac9b1ddb",
"title": "tif",
"description": null,
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
@@ -179,7 +179,7 @@
]
},
"kml": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/map-overlay.kml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120829Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=92f63b9f463041c4ed87b3cb508ba8c57fd2db8ea2d22026087ba57d2ef88f54",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/map-overlay.kml?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120829Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=92f63b9f463041c4ed87b3cb508ba8c57fd2db8ea2d22026087ba57d2ef88f54",
"title": "kml",
"description": null,
"type": "application/kml",
@@ -188,7 +188,7 @@
]
},
"thumbnail": {
"href": "http://192.168.1.8:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/quick-look.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120829Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=19bebb1e446add1d6e0898b4329d2a3e016f59087719ce2b51dff5c487835cb0",
"href": "http://192.168.60.201:9000/stac-data/S1A/S1A_IW_GRDH_1SDV_20250625T013854_20250625T013919_059796_076D15_B5C1.SAFE/quick-look.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T120829Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=19bebb1e446add1d6e0898b4329d2a3e016f59087719ce2b51dff5c487835cb0",
"title": "png",
"description": null,
"type": "image/png",
@@ -27,7 +27,6 @@ import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.kotlin.KotlinModule
import java.math.BigDecimal
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.Optional
import java.util.UUID
@@ -220,9 +219,9 @@ class RoutePassportIngestionServiceTest {
status = RoutePassportOutboxStatus.PENDING,
attempts = 0,
maxAttempts = 10,
nextAttemptAt = LocalDateTime.now(ZoneOffset.UTC),
createdAt = LocalDateTime.now(ZoneOffset.UTC),
updatedAt = LocalDateTime.now(ZoneOffset.UTC),
nextAttemptAt = LocalDateTime.now(),
createdAt = LocalDateTime.now(),
updatedAt = LocalDateTime.now(),
)
private fun fixture(): IngestionFixture {
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
import { TguApiError } from "../model/tguTypes";
import { toLocalIso } from "../utils/localTime";
const BASE = "/api/pcp-ballistics";
@@ -152,9 +153,9 @@ export async function fetchSatelliteRva(
return JSON.parse(text) as RvaZone[];
}
/** LocalDateTime без зоны: YYYY-MM-DDTHH:mm:ss. */
/** LocalDateTime без зоны (локальные настенные часы): YYYY-MM-DDTHH:mm:ss. */
function toLocalDateTime(ms: number): string {
return new Date(ms).toISOString().slice(0, 19);
return toLocalIso(ms);
}
function extractErrorText(body: string): string {
+3 -3
View File
@@ -5,6 +5,7 @@ import {
type TguPlanDecisionMessage,
type TguPlatform
} from "../model/tguTypes";
import { toLocalIso } from "../utils/localTime";
export type FlightLinePoint = {
time: string;
@@ -27,10 +28,9 @@ export async function fetchFlightLine(
fromMs: number,
toMs: number
): Promise<FlightLinePoint[]> {
const toLocalDT = (ms: number) => new Date(ms).toISOString().slice(0, 19);
const query = new URLSearchParams({
time_start: toLocalDT(fromMs),
time_stop: toLocalDT(toMs)
time_start: toLocalIso(fromMs),
time_stop: toLocalIso(toMs)
});
return fetchJson<FlightLinePoint[]>(
`/api/tgu-planning/spacecraft/${encodeURIComponent(noradId)}/flight-line?${query}`
@@ -11,7 +11,7 @@ export type DraftMode = "replace" | "fork-forward";
export type CreateDraftRequest = {
mode: DraftMode;
/** Начало окна целевой ЗРВ (UTC naive, ISO без зоны). Обязательно для fork-forward. */
/** Начало окна целевой ЗРВ (naive, ISO без зоны, локальное). Обязательно для fork-forward. */
windowStart?: string;
/** Конец окна целевой ЗРВ. Обязательно для fork-forward. */
windowStop?: string;
@@ -53,7 +53,7 @@ export type ZrvWindowResponse = {
/**
* Запрос создания плана новой цепочки (вкладка «Планы»). Окно набирается из двух ЗРВ:
* `windowStart`/`windowKppId` старт выбранной стартовой зоны, `windowStop` конец конечной.
* Времена UTC naive (ISO без зоны), как ждёт `LocalDateTime` бэка. `basePlanId` предыстория
* Времена naive (ISO без зоны, локальное), как ждёт `LocalDateTime` бэка. `basePlanId` предыстория
* (история тянется) либо `null` (цепочка без истории, пустой план).
*/
export type CreateChainPlanRequest = {
@@ -1,7 +1,7 @@
import { useEffect, useState, type CSSProperties } from "react";
/**
* Ввод времени суток в строгом 24-часовом формате `ЧЧ:ММ[:СС]` (UTC).
* Ввод времени суток в строгом 24-часовом формате `ЧЧ:ММ[:СС]` (локальное).
*
* Нативные `<input type="time">`/`type="datetime-local"` рендерят время по локали
* браузера/ОС (часто AM/PM) и не подчиняются атрибуту `lang`, поэтому время вводим
@@ -101,7 +101,7 @@ export function Time24Field({ value, withSeconds = false, onChange, className, s
/**
* Замена нативного `<input type="datetime-local">`: нативный календарь для даты
* (у даты нет проблемы AM/PM) + [Time24Field] для времени в 24-часовом формате.
* Контракт значения тот же строка `YYYY-MM-DDTHH:mm` (UTC), как у datetime-local.
* Контракт значения тот же строка `YYYY-MM-DDTHH:mm` (локальное), как у datetime-local.
*/
type DateTime24FieldProps = {
value: string;
@@ -1,6 +1,6 @@
import { formatDate, formatDateTime, formatDuration, formatTime, kindColor, kindLabel } from "./editorPresentation";
import { statusStyle } from "../tgu-planning/tguStatus";
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
import { formatLocalDateTime, parseLocal, toLocalIso } from "../../utils/localTime";
import { Time24Field } from "../../components/timeFields";
import type { RvaZone } from "../../api/ballisticsApi";
import type { SurveyRoute } from "../../api/missionPlanningApi";
@@ -50,7 +50,7 @@ type ShootPanelProps = {
selectedPass?: EditorVisibilityWindow;
modeId?: string;
modes: EditorMode[];
/** Окно строящегося маршрута (UTC, мс). undefined пока не выбран проход. */
/** Окно строящегося маршрута (локальное, мс). undefined пока не выбран проход. */
routeWindow?: { startMs: number; endMs: number };
/** Текущий крен (gamma) маршрута, градусы. */
routeRoll?: number;
@@ -105,7 +105,7 @@ type DropPanelProps = {
onSelectZone: (zone?: RvaZone) => void;
/** Название станции по её номеру (из каталога станций). */
stationName: (stationId: number) => string;
/** Время начала зоны в мс (UTC) — для подписи. */
/** Время начала зоны в мс (локальное) — для подписи. */
zoneStartMs: (zone: RvaZone) => number;
zoneStopMs: (zone: RvaZone) => number;
/** Съёмки-кандидаты для выбранной зоны (end < начало зоны). */
@@ -699,7 +699,7 @@ function surveyTypeLabel(surveyType: string): string {
}
function formatIso(iso: string): string {
return formatUtcDateTime(parseUtc(iso));
return formatLocalDateTime(parseLocal(iso));
}
function PanelHead({ title, onClose }: { title: string; onClose: () => void }) {
@@ -747,7 +747,7 @@ function TimeStepper({
/**
* Степпер времени окна маршрута с секундной точностью: кнопки ±5 c / ±30 c и ручной
* ввод времени `чч:мм:сс` (UTC, без даты дата берётся из текущего значения).
* ввод времени `чч:мм:сс` (локальное, без даты дата берётся из текущего значения).
* Значение зажимается в [min, max].
*/
function RouteTimeStepper({
@@ -766,16 +766,16 @@ function RouteTimeStepper({
const clamp = (next: number) => Math.max(min ?? Number.NEGATIVE_INFINITY, Math.min(max ?? Number.POSITIVE_INFINITY, next));
const change = (deltaMs: number) => onChange(clamp(value + deltaMs));
const onTimeChange = (raw: string) => {
// Ввод — только время суток (ЧЧ:ММ:СС, UTC); день оставляем прежним.
const datePart = new Date(value).toISOString().slice(0, 10);
const ms = parseUtc(`${datePart}T${raw}`);
// Ввод — только время суток (ЧЧ:ММ:СС, локальное); день оставляем прежним.
const datePart = toLocalIso(value).slice(0, 10);
const ms = parseLocal(`${datePart}T${raw}`);
if (Number.isFinite(ms)) onChange(clamp(ms));
};
return (
<label className="tgu-editor-time-stepper tgu-editor-time-stepper--seconds">
<span>{label}</span>
<Time24Field withSeconds value={new Date(value).toISOString().slice(11, 19)} onChange={onTimeChange} />
<Time24Field withSeconds value={toLocalIso(value).slice(11, 19)} onChange={onTimeChange} />
<div className="tgu-editor-time-stepper__steps">
<button type="button" onClick={() => change(-30_000)}>30с</button>
<button type="button" onClick={() => change(-5_000)}>5с</button>
@@ -85,7 +85,7 @@ export function EditorTimeline({
<div className="tgu-editor-timeline" ref={wrapperRef} onClick={onClearSelection}>
<div className="tgu-editor-timeline__surface" style={{ width: LABEL_WIDTH + bodyWidth, height: Math.max(220, surfaceHeight) }}>
<div className="tgu-editor-timeline__axis-label" style={{ width: LABEL_WIDTH }}>
Время (UTC)
Время
</div>
<div className="tgu-editor-timeline__axis" style={{ left: LABEL_WIDTH, width: bodyWidth }}>
{days.map((day) => (
@@ -206,7 +206,7 @@ export function EditorTimeline({
function buildDays(window: EditorTimelineWindow): number[] {
const result: number[] = [];
const start = new Date(window.fromMs);
let dayMs = Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate());
let dayMs = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime();
while (dayMs <= window.toMs) {
result.push(dayMs);
dayMs += 24 * 60 * 60 * 1000;
@@ -9,14 +9,14 @@ type MapPassSelectorProps = {
onToChange: (idx: number) => void;
};
function utcHM(ms: number): string {
function localHM(ms: number): string {
const d = new Date(ms);
return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
}
function utcDM(ms: number): string {
function localDM(ms: number): string {
const d = new Date(ms);
return `${String(d.getUTCDate()).padStart(2, "0")}.${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}`;
}
export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onToChange }: MapPassSelectorProps) {
@@ -27,7 +27,7 @@ export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onTo
const fromPass = passes[Math.max(0, Math.min(fromIndex - 1, n - 1))];
const toPass = passes[Math.max(0, Math.min(toIndex - 1, n - 1))];
const multiDay = utcDM(passes[0].startMs) !== utcDM(passes[n - 1].startMs);
const multiDay = localDM(passes[0].startMs) !== localDM(passes[n - 1].startMs);
const isFullRange = fromIndex === 1 && toIndex === n;
const fromRev = fromPass.revolution ?? fromIndex;
const toRev = toPass.revolution ?? toIndex;
@@ -76,10 +76,9 @@ export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onTo
<span className="map-pass-selector__title">Витки</span>
<span className="map-pass-selector__nums">{fromRev}{toRev}</span>
<span className="map-pass-selector__times">
{multiDay ? `${utcDM(fromPass.startMs)} ` : ""}{utcHM(fromPass.startMs)}
{multiDay ? `${localDM(fromPass.startMs)} ` : ""}{localHM(fromPass.startMs)}
{" "}
{multiDay ? `${utcDM(toPass.startMs)} ` : ""}{utcHM(toPass.startMs)}
{" UTC"}
{multiDay ? `${localDM(toPass.startMs)} ` : ""}{localHM(toPass.startMs)}
</span>
{!isFullRange && (
<button
@@ -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 }));
@@ -14,17 +14,17 @@ import {
} from "./editorDraft";
import type { PlanMode } from "./editorApi";
import type { DraftHistory } from "./model/editorTypes";
import { parseUtc } from "../../utils/utcTime";
import { parseLocal } from "../../utils/localTime";
const plan: TguPlanUi = {
planId: "plan-1",
spacecraftId: "56756",
startTime: "2026-05-29T01:00:00Z",
endTime: "2026-05-29T02:00:00Z",
startTime: "2026-05-29T01:00:00",
endTime: "2026-05-29T02:00:00",
kppId: "KPP-1",
status: "PLANNED",
startMs: parseUtc("2026-05-29T01:00:00Z"),
endMs: parseUtc("2026-05-29T02:00:00Z")
startMs: parseLocal("2026-05-29T01:00:00"),
endMs: parseLocal("2026-05-29T02:00:00")
};
describe("editorDraft", () => {
@@ -85,8 +85,8 @@ describe("editorDraft", () => {
id: "survey-11",
kind: "shoot",
label: "Съёмка · виток 4321",
startMs: parseUtc("2026-05-29T01:10:00Z"),
endMs: parseUtc("2026-05-29T01:10:00Z") + 180_000,
startMs: parseLocal("2026-05-29T01:10:00"),
endMs: parseLocal("2026-05-29T01:10:00") + 180_000,
origin: "plan",
planModeId: 11,
revolution: 4321,
@@ -100,8 +100,8 @@ describe("editorDraft", () => {
id: "drop-22",
kind: "downlink",
label: "Сброс · 102",
startMs: parseUtc("2026-05-29T01:40:00Z"),
endMs: parseUtc("2026-05-29T01:40:00Z") + 120_000,
startMs: parseLocal("2026-05-29T01:40:00"),
endMs: parseLocal("2026-05-29T01:40:00") + 120_000,
origin: "plan",
planModeId: 22,
revolution: 4322,
@@ -1,5 +1,5 @@
import type { TguPlanUi } from "../../model/timelineTypes";
import { parseUtc } from "../../utils/utcTime";
import { parseLocal } from "../../utils/localTime";
import type { PlanMode } from "./editorApi";
import type {
Draft,
@@ -64,7 +64,7 @@ export function seedDraft(plan?: TguPlanUi, spacecraftId?: string): Draft {
export function planModesToWorks(modes: PlanMode[]): EditorWork[] {
return modes.flatMap((mode): EditorWork[] => {
if (mode.timeStart == null) return [];
const startMs = parseUtc(mode.timeStart);
const startMs = parseLocal(mode.timeStart);
if (!Number.isFinite(startMs)) return [];
const endMs = startMs + Math.max(1, mode.duration) * 1000;
@@ -272,7 +272,7 @@ function readTime(record: Record<string, unknown>, key: string): number | undefi
return value;
}
if (typeof value === "string") {
const parsed = parseUtc(value);
const parsed = parseLocal(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
@@ -24,12 +24,12 @@ export function kindColor(kind: EditorWorkKind): string {
export function formatTime(ms: number): string {
const date = new Date(ms);
return `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
export function formatDate(ms: number): string {
const date = new Date(ms);
return `${pad(date.getUTCDate())} ${EDITOR_MONTHS[date.getUTCMonth()]}`;
return `${pad(date.getDate())} ${EDITOR_MONTHS[date.getMonth()]}`;
}
export function formatDateTime(ms: number): string {
@@ -1,6 +1,6 @@
import type { RequestMapItem } from "../../model/requestTypes";
import type { MapPolygon } from "./model/mapTypes";
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
export type RequestAtPoint = { polygon: MapPolygon; item: RequestMapItem };
@@ -96,18 +96,18 @@ export function Tgu2DMapRequestsPanel({ requests, selectedPolygonId, onHover, on
}
function formatDateTime(value: string): string {
return formatUtcDateTime(parseUtc(value));
return formatLocalDateTime(parseLocal(value));
}
function formatDateRange(begin: string, end: string): string {
const b = new Date(parseUtc(begin));
const e = new Date(parseUtc(end));
const b = new Date(parseLocal(begin));
const e = new Date(parseLocal(end));
const sameDay =
b.getUTCFullYear() === e.getUTCFullYear() &&
b.getUTCMonth() === e.getUTCMonth() &&
b.getUTCDate() === e.getUTCDate();
const dateOpts: Intl.DateTimeFormatOptions = { timeZone: "UTC", day: "2-digit", month: "2-digit" };
const timeOpts: Intl.DateTimeFormatOptions = { timeZone: "UTC", hour: "2-digit", minute: "2-digit" };
b.getFullYear() === e.getFullYear() &&
b.getMonth() === e.getMonth() &&
b.getDate() === e.getDate();
const dateOpts: Intl.DateTimeFormatOptions = { day: "2-digit", month: "2-digit" };
const timeOpts: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit" };
const beginStr = `${b.toLocaleDateString("ru-RU", dateOpts)} ${b.toLocaleTimeString("ru-RU", timeOpts)}`;
const endStr = sameDay
? b.toLocaleTimeString("ru-RU", timeOpts)
@@ -3,7 +3,7 @@ import { filterPlatforms, platformLabel } from "../tgu-planning/tguTimelineMappe
import { TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
import type { Tgu2DMapLayerAvailability } from "./Tgu2DMapLayers";
import type { RequestsState } from "./Tgu2DMapTab";
import { parseUtc } from "../../utils/utcTime";
import { parseLocal } from "../../utils/localTime";
import { DateTime24Field } from "../../components/timeFields";
const DAY_MS = 24 * 60 * 60 * 1000;
@@ -48,7 +48,7 @@ export function Tgu2DMapSidebar({
const quickToday = () => {
const now = new Date();
const fromMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const fromMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
onRequestsQuickRange(fromMs, fromMs + DAY_MS);
};
@@ -62,8 +62,8 @@ export function Tgu2DMapSidebar({
onRequestsQuickRange(fromMs, fromMs + 30 * DAY_MS);
};
const requestsFromMs = parseUtc(requestsState.fromValue);
const requestsToMs = parseUtc(requestsState.toValue);
const requestsFromMs = parseLocal(requestsState.fromValue);
const requestsToMs = parseLocal(requestsState.toValue);
const requestsRangeInvalid =
!Number.isFinite(requestsFromMs) ||
!Number.isFinite(requestsToMs) ||
@@ -15,7 +15,7 @@ import { Tgu2DMapStationPanel } from "./Tgu2DMapStationPanel";
import { Tgu2DMapToolbar } from "./Tgu2DMapToolbar";
import { Tgu2DMapView } from "./Tgu2DMapView";
import { getTgu2DMapIntervalState, TGU_2D_MAP_INTERVAL_WARNING } from "./tgu2DMapInterval";
import { parseUtc, toUtcInputValue } from "../../utils/utcTime";
import { parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
type Tgu2DMapTabProps = {
range: TimelineRange;
@@ -38,8 +38,8 @@ export type RequestsState = {
function buildInitialRequestsState(range: TimelineRange): RequestsState {
return {
fromValue: toUtcInputValue(range.fromMs),
toValue: toUtcInputValue(range.toMs),
fromValue: toLocalInputValue(range.fromMs),
toValue: toLocalInputValue(range.toMs),
items: [],
loading: false,
loadedCount: 0,
@@ -78,16 +78,16 @@ export function Tgu2DMapTab({
const mapInvalidRange = invalidRange || mapInterval.tooLarge;
const loadRequests = useCallback(async (fromValue: string, toValue: string) => {
const fromMs = parseUtc(fromValue);
const toMs = parseUtc(toValue);
const fromMs = parseLocal(fromValue);
const toMs = parseLocal(toValue);
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs) return;
setRequestsState((current) => ({ ...current, loading: true, error: undefined, loadedCount: 0 }));
try {
const result = await fetchRequestsForMap(
{
endFrom: new Date(fromMs).toISOString().slice(0, 19),
beginTo: new Date(toMs).toISOString().slice(0, 19)
endFrom: toLocalIso(fromMs),
beginTo: toLocalIso(toMs)
},
(loaded, total) => {
setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total }));
@@ -124,8 +124,8 @@ export function Tgu2DMapTab({
}, []);
const setRequestsQuickRange = useCallback((fromMs: number, toMs: number) => {
const fromValue = toUtcInputValue(fromMs);
const toValue = toUtcInputValue(toMs);
const fromValue = toLocalInputValue(fromMs);
const toValue = toLocalInputValue(toMs);
setRequestsState((current) => ({ ...current, fromValue, toValue }));
void loadRequests(fromValue, toValue);
}, [loadRequests]);
@@ -297,7 +297,6 @@ export function Tgu2DMapTab({
function formatDate(ms: number): string {
return new Date(ms).toLocaleString("ru-RU", {
timeZone: "UTC",
day: "2-digit",
month: "2-digit",
hour: "2-digit",
@@ -2,7 +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";
import { parseLocal } from "../../utils/localTime";
function plan(startTime: string, endTime: string): TguPlanUi {
return {
@@ -12,16 +12,16 @@ function plan(startTime: string, endTime: string): TguPlanUi {
endTime,
kppId: "KPP-1",
status: "PLANNED",
startMs: parseUtc(startTime),
endMs: parseUtc(endTime)
startMs: parseLocal(startTime),
endMs: parseLocal(endTime)
};
}
describe("tgu2DMapInterval", () => {
it("marks common map interval longer than 7 days as too large", () => {
const state = getTgu2DMapIntervalState({
fromMs: parseUtc("2026-05-29T00:00:00"),
toMs: parseUtc("2026-06-05T00:00:01")
fromMs: parseLocal("2026-05-29T00:00:00"),
toMs: parseLocal("2026-06-05T00:00:01")
});
expect(state.tooLarge).toBe(true);
@@ -31,8 +31,8 @@ describe("tgu2DMapInterval", () => {
const selectedPlan = plan("2026-05-29T01:00:00", "2026-06-07T02:00:00");
const state = getTgu2DMapIntervalState(
{
fromMs: parseUtc("2026-05-29T00:00:00"),
toMs: parseUtc("2026-06-10T00:00:00")
fromMs: parseLocal("2026-05-29T00:00:00"),
toMs: parseLocal("2026-06-10T00:00:00")
},
selectedPlan
);
@@ -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,
@@ -6,7 +6,7 @@ import type {
RsaParams,
RsaResultType
} from "../../model/requestTypes";
import { parseUtc } from "../../utils/utcTime";
import { parseLocal } from "../../utils/localTime";
import { DateTime24Field } from "../../components/timeFields";
export type RequestFormSubmit = {
@@ -87,8 +87,8 @@ export function RequestCreatePanel({
const [rsaResolution, setRsaResolution] = useState("1");
const importanceNum = Number(importance);
const beginMs = parseUtc(begin);
const endMs = parseUtc(end);
const beginMs = parseLocal(begin);
const endMs = parseLocal(end);
const kppList = parseKpp(kpp);
const kppValid = kppList !== null;
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import type { RequestAtPoint } from "../tgu-map-2d/Tgu2DMapRequestsPanel";
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
type Props = {
requests: RequestAtPoint[];
@@ -155,7 +155,7 @@ export function RequestDetailsPanel({
}
function formatDateTime(value: string): string {
return formatUtcDateTime(parseUtc(value));
return formatLocalDateTime(parseLocal(value));
}
function surveyTypeLabel(surveyType: string): string {
@@ -9,7 +9,7 @@ import type { RequestAtPoint } from "../tgu-map-2d/Tgu2DMapRequestsPanel";
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
import type { GeoPoint, MapPolygon } from "../tgu-map-2d/model/mapTypes";
import { generateUuid } from "../../utils/uuid";
import { parseUtc, toUtcInputValue, toUtcIso } from "../../utils/utcTime";
import { parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime";
import { DateTime24Field } from "../../components/timeFields";
import { RequestCreatePanel, type RequestFormSubmit } from "./RequestCreatePanel";
import { RequestDetailsPanel } from "./RequestDetailsPanel";
@@ -37,8 +37,8 @@ type PanelMode = "none" | "details" | "create";
function buildInitialLoadState(range: TimelineRange): RequestsLoadState {
return {
fromValue: toUtcInputValue(range.fromMs),
toValue: toUtcInputValue(range.toMs),
fromValue: toLocalInputValue(range.fromMs),
toValue: toLocalInputValue(range.toMs),
items: [],
loading: false,
loadedCount: 0,
@@ -75,14 +75,14 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
}, []);
const loadRequests = useCallback(async (fromValue: string, toValue: string) => {
const fromMs = parseUtc(fromValue);
const toMs = parseUtc(toValue);
const fromMs = parseLocal(fromValue);
const toMs = parseLocal(toValue);
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs) return;
setRequestsState((current) => ({ ...current, loading: true, error: undefined, loadedCount: 0 }));
try {
const result = await fetchRequestsForMap(
{ endFrom: new Date(fromMs).toISOString().slice(0, 19), beginTo: new Date(toMs).toISOString().slice(0, 19) },
{ endFrom: toLocalIso(fromMs), beginTo: toLocalIso(toMs) },
(loaded, total) => setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total }))
);
setRequestsState((current) => ({
@@ -204,8 +204,8 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
name: form.name,
geometry,
importance: form.importance,
beginDateTime: toUtcIso(form.beginDateTime),
endDateTime: toUtcIso(form.endDateTime),
beginDateTime: toLocalIso(parseLocal(form.beginDateTime)),
endDateTime: toLocalIso(parseLocal(form.endDateTime)),
kpp: form.kpp,
highPriorityTransmit: form.highPriorityTransmit,
optics: form.optics,
@@ -244,8 +244,8 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
[closeAll, loadRequests, requestsState.fromValue, requestsState.toValue]
);
const fromMs = parseUtc(requestsState.fromValue);
const toMs = parseUtc(requestsState.toValue);
const fromMs = parseLocal(requestsState.fromValue);
const toMs = parseLocal(requestsState.toValue);
const rangeInvalid = !Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs;
const hasPanel = panelMode !== "none";
@@ -0,0 +1,57 @@
/**
* Единый разбор и форматирование времени в локальной зоне.
*
* Бэкенд (Kotlin `LocalDateTime`) отдаёт время как ISO-строку без зоны
* (например `"2026-05-29T01:00:00"`) это настенные часы локальной зоны, в которой
* работают все сервисы, без UTC-семантики. Браузер по умолчанию трактует такую naive-строку
* как локальное время это нам и нужно: разбор, показ и ввод идут в зоне браузера, а
* сериализация назад отдаёт те же настенные часы naive-строкой без зоны.
*
* Строки с явной зоной (`...Z`/офсет, напр. валидность платформ из НСИ) парсятся как
* абсолютный момент и показываются в локальной зоне.
*/
function pad(n: number): string {
return String(n).padStart(2, "0");
}
/** Разбирает ISO-строку в эпоху-мс: naive — как локальное время, зонированную — как её момент. */
export function parseLocal(value: string): number {
return Date.parse(value);
}
/** Дата-время в локальной зоне, формат ru-RU. */
export function formatLocalDateTime(ms: number): string {
return new Date(ms).toLocaleString("ru-RU", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
});
}
/** Дата (дд.мм) в локальной зоне. */
export function formatLocalDate(ms: number): string {
return new Date(ms).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit" });
}
/** Время (чч:мм) в локальной зоне. */
export function formatLocalTime(ms: number): string {
return new Date(ms).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
}
/** Значение для пикеров (`YYYY-MM-DDTHH:mm`) из локальных компонент. */
export function toLocalInputValue(ms: number): string {
const d = new Date(ms);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
/**
* naive `LocalDateTime` без зоны (`YYYY-MM-DDTHH:mm:ss`) из локальных компонент эпохи-мс.
* Все бэкенды ожидают `LocalDateTime` без зоны настенные часы локальной зоны.
*/
export function toLocalIso(ms: number): string {
const d = new Date(ms);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
@@ -1,62 +0,0 @@
import { readdirSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { describe, expect, it } from "vitest";
/**
* Страж инварианта «всё время UTC» (см. docs/standards/DATETIME_UTC.md).
*
* Сканирует исходники и падает на паттернах, которые трактуют время в поясе
* браузера: «голый» разбор строки (`new Date("...")`, `Date.parse`) и показ через
* `toLocale*` без `timeZone`. Парсинг и форматирование времени бэка должны идти
* только через `utils/utcTime` (`parseUtc`, `formatUtc*`). Этот файл и сам
* `utcTime.ts` единственные исключения: в них паттерны присутствуют легитимно
* (как реализация утилиты и как строки-описания запретов).
*/
const SRC_DIR = join(process.cwd(), "src");
// Исключения: сам страж (содержит паттерны как строки-описания) и каноническая
// утилита utcTime.ts (легитимная реализация parseUtc/formatUtc*).
const EXCLUDED = ["utcTime.ts", "utcTime.guard.test.ts"];
interface Rule {
readonly name: string;
readonly pattern: RegExp;
}
const RULES: Rule[] = [
{ name: "Date.parse(...) — парсит строку в поясе браузера; используйте parseUtc", pattern: /\bDate\.parse\s*\(/ },
{ name: "new Date(\"...\") со строкой — трактует naive-строку как локальное время; используйте parseUtc", pattern: /new\s+Date\(\s*['"`]/ },
{ name: "formatDateTimeLocal — локальный хелпер пикеров; используйте toUtcInputValue/parseUtc", pattern: /\bformatDateTimeLocal\b/ },
// toLocale* без опций или только с локалью → пояс браузера.
{ name: "toLocale*() без timeZone — показывает в поясе браузера; используйте formatUtc* или { timeZone: \"UTC\" }", pattern: /\.toLocale(String|DateString|TimeString)\(\s*(['"`][^'"`]*['"`]\s*)?\)/ },
// toLocale* с inline-объектом опций, в котором нет timeZone.
{ name: "toLocale*({...}) без timeZone в опциях — показывает в поясе браузера; добавьте timeZone: \"UTC\"", pattern: /\.toLocale(String|DateString|TimeString)\(\s*['"`][^'"`]*['"`]\s*,\s*\{(?![\s\S]*?timeZone)[\s\S]*?\}/ }
];
function collectSourceFiles(dir: string): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...collectSourceFiles(full));
} else if (/\.tsx?$/.test(entry.name) && !EXCLUDED.includes(entry.name)) {
out.push(full);
}
}
return out;
}
describe("UTC time invariant", () => {
const files = collectSourceFiles(SRC_DIR);
it("scans the source tree", () => {
expect(files.length).toBeGreaterThan(0);
});
for (const rule of RULES) {
it(`forbids: ${rule.name}`, () => {
const offenders = files.filter((file) => rule.pattern.test(readFileSync(file, "utf8")));
expect(offenders.map((file) => relative(SRC_DIR, file))).toEqual([]);
});
}
});
@@ -1,50 +0,0 @@
/**
* Единый разбор и форматирование времени в UTC.
*
* Бэкенд (Kotlin `LocalDateTime`) отдаёт время как ISO-строку без зоны
* (например `"2026-05-29T01:00:00"`) и по соглашению системы это UTC.
* Конструктор `Date`/`Date.parse` трактует такую строку как локальное время
* браузера, поэтому naive-строки нужно парсить явно как UTC, а отображать
* с `timeZone: "UTC"`. Весь интерфейс ТГУ работает в UTC.
*/
/** Разбирает ISO-строку как UTC: если зоны нет — добавляет `Z`. */
export function parseUtc(value: string): number {
const hasZone = /[zZ]$|[+-]\d{2}:?\d{2}$/.test(value);
return Date.parse(hasZone ? value : `${value}Z`);
}
/** Дата-время в UTC, формат ru-RU. */
export function formatUtcDateTime(ms: number): string {
return new Date(ms).toLocaleString("ru-RU", {
timeZone: "UTC",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
});
}
/** Дата (дд.мм) в UTC. */
export function formatUtcDate(ms: number): string {
return new Date(ms).toLocaleDateString("ru-RU", { timeZone: "UTC", day: "2-digit", month: "2-digit" });
}
/** Время (чч:мм) в UTC. */
export function formatUtcTime(ms: number): string {
return new Date(ms).toLocaleTimeString("ru-RU", { timeZone: "UTC", hour: "2-digit", minute: "2-digit" });
}
/** Значение для `<input type="datetime-local">` в UTC (`YYYY-MM-DDTHH:mm`). */
export function toUtcInputValue(ms: number): string {
return new Date(ms).toISOString().slice(0, 16);
}
/**
* Naive-строку (из `datetime-local`) возвращает как naive ISO без зоны (`YYYY-MM-DDTHH:mm:ss`).
* Все бэкенды ожидают `LocalDateTime` без зоны.
*/
export function toUtcIso(value: string): string {
return new Date(parseUtc(value)).toISOString().slice(0, 19);
}
File diff suppressed because one or more lines are too long
@@ -12,9 +12,9 @@ object DraftMode {
data class CreateDraftRequest(
/** [DraftMode.REPLACE] — черновик в том же слоте; [DraftMode.FORK_FORWARD] — черновик на следующей ЗРВ. */
val mode: String,
/** Начало окна целевой ЗРВ (UTC naive). Обязательно для fork-forward. */
/** Начало окна целевой ЗРВ (naive, локальное время). Обязательно для fork-forward. */
val windowStart: LocalDateTime? = null,
/** Конец окна целевой ЗРВ (UTC naive). Обязательно для fork-forward. */
/** Конец окна целевой ЗРВ (naive, локальное время). Обязательно для fork-forward. */
val windowStop: LocalDateTime? = null,
/** КПП целевой ЗРВ. Обязательно для fork-forward. */
val windowKppId: String? = null
@@ -33,9 +33,9 @@ data class CreateChainPlanRequest(
val spacecraftId: String,
/** Прошлый план-предыстория или null для цепочки без истории. */
val basePlanId: UUID? = null,
/** Начало окна (UTC naive) — старт выбранной стартовой ЗРВ. */
/** Начало окна (naive, локальное время) — старт выбранной стартовой ЗРВ. */
val windowStart: LocalDateTime,
/** Конец окна (UTC naive) — конец выбранной конечной ЗРВ. */
/** Конец окна (naive, локальное время) — конец выбранной конечной ЗРВ. */
val windowStop: LocalDateTime,
/** КПП стартовой ЗРВ. */
val windowKppId: String
@@ -7,7 +7,6 @@ import jakarta.persistence.Enumerated
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -40,10 +39,10 @@ class AcceptedPlanSlotsUpdateJobEntity(
var lastError: String? = null,
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var updatedAt: LocalDateTime = LocalDateTime.now()
) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(planId = UUID.randomUUID())
@@ -7,7 +7,6 @@ import jakarta.persistence.Enumerated
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -46,10 +45,10 @@ class PlanIssueAttemptEntity(
var finishedAt: LocalDateTime? = null,
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var updatedAt: LocalDateTime = LocalDateTime.now()
) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(attemptId = UUID.randomUUID(), planId = UUID.randomUUID(), attemptNo = 0)
@@ -7,7 +7,6 @@ import jakarta.persistence.Enumerated
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -49,10 +48,10 @@ class PlannedPlanEntity(
var acceptedAt: LocalDateTime? = null,
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var updatedAt: LocalDateTime = LocalDateTime.now()
) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -19,7 +18,7 @@ class ProcessedPlanDecisionEventEntity(
var eventId: UUID,
@Column(name = "received_at", nullable = false)
var receivedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var receivedAt: LocalDateTime = LocalDateTime.now()
) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(eventId = UUID.randomUUID())
@@ -6,7 +6,6 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
@Entity
@@ -43,7 +42,7 @@ class SpacecraftPlanningStateEntity(
var pausedAfterReject: Boolean = false,
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var updatedAt: LocalDateTime = LocalDateTime.now()
) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(spacecraftId = "")
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Persisted snapshot КА из НСИ, используемый как вход для refresh jobs.
@@ -45,10 +44,10 @@ class SpacecraftSnapshotEntity(
var active: Boolean = true,
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var updatedAt: LocalDateTime = LocalDateTime.now()
) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(spacecraftId = "", noradId = 0)
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Полный raw snapshot окон видимости КА из баллистики.
@@ -41,10 +40,10 @@ class SpacecraftVisibilitySnapshotEntity(
var windowsCount: Int? = null,
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var updatedAt: LocalDateTime = LocalDateTime.now()
) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(spacecraftId = "", noradId = 0, payloadJson = "", payloadHash = "", fetchedAt = LocalDateTime.MIN)
@@ -7,7 +7,6 @@ import jakarta.persistence.Enumerated
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Job на перечитывание окон видимости одного КА у баллистики.
@@ -42,10 +41,10 @@ class VisibilityRefreshJobEntity(
var rerunRequested: Boolean = false,
@Column(name = "requested_at", nullable = false)
var requestedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
var requestedAt: LocalDateTime = LocalDateTime.now(),
@Column(name = "updated_at", nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
var updatedAt: LocalDateTime = LocalDateTime.now()
) {
/** Создаёт пустой экземпляр сущности, необходимый для JPA. */
constructor() : this(spacecraftId = "")
@@ -12,7 +12,6 @@ import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientException
import org.springframework.web.reactive.function.client.WebClientResponseException
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
/**
@@ -37,7 +36,7 @@ class ExternalPointsClient(
* которые разрешены для работы TGU.
*/
fun fetchAllSpacecraftPoints(): List<SpacecraftPoints> {
val timeStart = LocalDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.MINUTES)
val timeStart = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES)
val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays)
return platformService.loadAllowedPlatforms()
@@ -8,7 +8,7 @@ import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
import space.nstart.pcp_tgu_service.dto.ExternalRvaItemDto
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.ZoneId
import java.time.format.DateTimeParseException
import java.time.temporal.ChronoUnit
@@ -43,10 +43,11 @@ class VisibilityPayloadParser(
}
/**
* Разбирает UTC/ISO строку времени баллистики в naive `LocalDateTime` (по соглашению системы UTC).
* Разбирает ISO-строку времени баллистики в naive `LocalDateTime`.
*
* Момент из `Instant.parse` берём в UTC (а не в поясе JVM): naive-поля `planned_plan.start_time/end_time`
* по контракту хранят UTC, поэтому `atZone(systemDefault)` на не-UTC машине сдвигал бы время на офсет.
* Если время приходит с зоной (`...Z`/офсет), момент проецируем в локальный пояс JVM
* (`ZoneId.systemDefault()`): naive-время в системе настенные часы локальной зоны, в которой
* работают все сервисы, без UTC-семантики. Naive-строку без зоны берём как есть.
*
* Обрезаем до микросекунд это эффективная точность системы: `planned_plan.start_time/end_time`
* хранятся как Postgres `timestamp(6)`, при записи наносекундный хвост баллистики всё равно теряется.
@@ -55,7 +56,7 @@ class VisibilityPayloadParser(
*/
private fun toLocalDateTime(value: String): LocalDateTime =
try {
LocalDateTime.ofInstant(Instant.parse(value), ZoneOffset.UTC)
LocalDateTime.ofInstant(Instant.parse(value), ZoneId.systemDefault())
} catch (_: DateTimeParseException) {
LocalDateTime.parse(value)
}.truncatedTo(ChronoUnit.MICROS)
@@ -4,7 +4,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -17,6 +16,6 @@ class AcceptedPlanSlotsUpdateJobService(
/** Idempotent enqueue: один planId получает максимум одну job. */
@Transactional
fun enqueue(planId: UUID) {
acceptedPlanSlotsUpdateJobRepository.insertPendingIfAbsent(planId, LocalDateTime.now(ZoneOffset.UTC))
acceptedPlanSlotsUpdateJobRepository.insertPendingIfAbsent(planId, LocalDateTime.now())
}
}
@@ -10,7 +10,6 @@ import space.nstart.pcp_tgu_service.entity.AcceptedPlanSlotsUpdateJobStatus
import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobRepository
import java.net.InetAddress
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -36,7 +35,7 @@ class AcceptedPlanSlotsUpdateWorker(
/** Claim видим для тестов; в production защищён FOR UPDATE SKIP LOCKED. */
fun claimJobs(limit: Int): List<AcceptedPlanSlotsUpdateJobEntity> =
transactionTemplate.execute {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val jobs = acceptedPlanSlotsUpdateJobRepository.findClaimableForUpdate(now, limit)
jobs.forEach { job ->
job.status = AcceptedPlanSlotsUpdateJobStatus.RUNNING
@@ -70,7 +69,7 @@ class AcceptedPlanSlotsUpdateWorker(
private fun markDone(planId: UUID, processInstanceKey: Long) {
transactionTemplate.executeWithoutResult {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult
job.status = AcceptedPlanSlotsUpdateJobStatus.DONE
job.processInstanceKey = processInstanceKey
@@ -84,7 +83,7 @@ class AcceptedPlanSlotsUpdateWorker(
private fun markAmbiguous(planId: UUID, processInstanceKey: Long?, ex: Exception) {
transactionTemplate.executeWithoutResult {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult
job.status = AcceptedPlanSlotsUpdateJobStatus.AMBIGUOUS
job.processInstanceKey = processInstanceKey ?: job.processInstanceKey
@@ -17,7 +17,6 @@ import space.nstart.pcp_tgu_service.integration.api.MissionPlaningClient
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -47,7 +46,7 @@ class CreateChainPlanUseCase(
private val log = LoggerFactory.getLogger(javaClass)
fun createChainPlan(req: CreateChainPlanRequest): CreateChainPlanResponse {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
if (!req.windowStop.isAfter(req.windowStart)) {
throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "windowStop должен быть позже windowStart")
@@ -15,7 +15,6 @@ import space.nstart.pcp_tgu_service.integration.api.MissionPlaningClient
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -46,7 +45,7 @@ class CreateDraftUseCase(
val base = plannedPlanRepository.findById(basePlanId)
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Plan $basePlanId not found") }
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
// Идемпотентность: один открытый черновик на базовый план. Повторный клик «создать черновик»
// не плодит вторую миссию-клон и вторую DRAFT-строку.
@@ -19,7 +19,6 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Обрабатывает decision и закладочные feedback-события идемпотентно по eventId.
@@ -119,7 +118,7 @@ class HandlePlanDecisionUseCase(
return
}
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
plan.status = terminalStatus
plan.updatedAt = now
plannedPlanRepository.save(plan)
@@ -131,7 +130,7 @@ class HandlePlanDecisionUseCase(
message: PlanDecisionMessage,
attempt: PlanIssueAttemptEntity
) {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
attempt.status = when (message.decision) {
PlanDecision.ACCEPTED -> PlanIssueAttemptStatus.ACCEPTED
PlanDecision.REJECTED -> PlanIssueAttemptStatus.REJECTED
@@ -148,7 +147,7 @@ class HandlePlanDecisionUseCase(
plan: PlannedPlanEntity,
state: SpacecraftPlanningStateEntity
) {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
recordDecisionOnAttempt(message, attempt)
plan.status = PlannedPlanStatus.AWAITING_LAYIN
@@ -175,7 +174,7 @@ class HandlePlanDecisionUseCase(
plan: PlannedPlanEntity,
state: SpacecraftPlanningStateEntity
) {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
recordDecisionOnAttempt(message, attempt)
plan.status = PlannedPlanStatus.REJECTED
@@ -217,7 +216,7 @@ class HandlePlanDecisionUseCase(
}
private fun claimEvent(eventId: java.util.UUID): Boolean =
processedPlanDecisionEventRepository.insertIfAbsent(eventId, LocalDateTime.now(ZoneOffset.UTC)) != 0
processedPlanDecisionEventRepository.insertIfAbsent(eventId, LocalDateTime.now()) != 0
private fun getOrCreateLockedState(spacecraftId: String): SpacecraftPlanningStateEntity {
spacecraftPlanningStateJpaRepository.findById(spacecraftId).orElse(null)
@@ -7,7 +7,6 @@ import space.nstart.pcp_tgu_service.dto.StationParamsChangedMessage
import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* На изменение параметров станций перезапрашивает ЗРВ у баллистики по всем активным КА.
@@ -26,7 +25,7 @@ class HandleStationParamsChangedUseCase(
/** Идемпотентно по eventId; ставит refresh job по всем активным КА. */
@Transactional
fun handle(message: StationParamsChangedMessage) {
val claimed = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, LocalDateTime.now(ZoneOffset.UTC)) != 0
val claimed = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, LocalDateTime.now()) != 0
if (!claimed) {
return
}
@@ -19,7 +19,6 @@ import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -84,7 +83,7 @@ class IssueDuePlanWorker(
/** Готовит выдачу головы проекции для КА под lock'ом состояния (scale-out safe). */
private fun prepareDueForSpacecraft(spacecraftId: String): PreparedAttempt? {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val state = getOrCreateLockedState(spacecraftId)
if (state.activeAttemptId != null && !tryAdvancePastUnconfirmedPlan(state, now)) {
return null
@@ -163,7 +162,7 @@ class IssueDuePlanWorker(
}
private fun prepareSpecificPlan(planId: UUID): PreparedAttempt? {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val plan = plannedPlanRepository.findById(planId).orElse(null) ?: return null
val state = getOrCreateLockedState(plan.spacecraftId)
assertReissuableInSlot(plan, state, now)
@@ -185,7 +184,7 @@ class IssueDuePlanWorker(
}
private fun prepareDraftPromotion(draftPlanId: UUID): PreparedAttempt? {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val plan = plannedPlanRepository.findById(draftPlanId).orElse(null) ?: return null
if (plan.status != PlannedPlanStatus.DRAFT) {
throw ResponseStatusException(
@@ -342,7 +341,7 @@ class IssueDuePlanWorker(
private fun markStarted(response: ProcessStartResponse) {
transactionTemplate.executeWithoutResult {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val attempt = planIssueAttemptRepository.findById(response.attemptId).orElse(null) ?: return@executeWithoutResult
val plan = plannedPlanRepository.findById(response.planId).orElse(null) ?: return@executeWithoutResult
@@ -366,7 +365,7 @@ class IssueDuePlanWorker(
private fun markAmbiguous(preparedAttempt: PreparedAttempt) {
transactionTemplate.executeWithoutResult {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val attempt = planIssueAttemptRepository.findById(preparedAttempt.attemptId).orElse(null) ?: return@executeWithoutResult
val plan = plannedPlanRepository.findById(preparedAttempt.planId).orElse(null) ?: return@executeWithoutResult
attempt.status = PlanIssueAttemptStatus.AMBIGUOUS
@@ -11,7 +11,6 @@ import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -81,7 +80,7 @@ class PlanQueryService(
.sortedBy { response -> response.startTime }
private fun historySince(historyDays: Long): LocalDateTime =
LocalDateTime.now(ZoneOffset.UTC).minusDays(historyDays.coerceAtLeast(0))
LocalDateTime.now().minusDays(historyDays.coerceAtLeast(0))
private fun PlannedPlanEntity.businessKey(): String =
"$spacecraftId|$startTime|$endTime|$kppId"
@@ -7,7 +7,6 @@ import space.nstart.pcp_tgu_service.integration.api.VisibilityPayloadParser
import space.nstart.pcp_tgu_service.repository.SpacecraftVisibilitySnapshotRepository
import java.security.MessageDigest
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Сохраняет raw visibility payload. Расписание считается on-demand из снапшота
@@ -26,7 +25,7 @@ class SpacecraftVisibilitySnapshotService(
val payloadHash = sha256(payloadJson)
val existing = spacecraftVisibilitySnapshotRepository.findById(spacecraftId).orElse(null)
val metadata = runCatching { extractMetadata(spacecraftId, payloadJson) }.getOrNull()
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
if (existing == null) {
spacecraftVisibilitySnapshotRepository.save(
@@ -5,7 +5,7 @@ import org.springframework.transaction.support.TransactionTemplate
import space.nstart.pcp_tgu_service.domain.PlatformCatalogItem
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.ZoneId
/**
* Синхронизирует разрешённые КА из НСИ в БД и ставит refresh jobs.
@@ -26,7 +26,7 @@ class SyncSpacecraftFromNsiUseCase(
}
private fun persistAllowedSpacecraft(allowedPlatforms: List<PlatformCatalogItem>) {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val allowedSpacecraftIds = allowedPlatforms.mapNotNull { platform -> platform.noradId?.toString() }
allowedPlatforms.forEach { platform ->
@@ -40,10 +40,10 @@ class SyncSpacecraftFromNsiUseCase(
name = platform.name,
status = platform.status,
mission = platform.mission,
// validFrom/validTo приходят как Instant; в naive-поля БД пишем UTC-стенку, а не пояс JVM
// (по соглашению системы naive LocalDateTime = UTC) — иначе на не-UTC машине время уезжало бы.
validFrom = platform.validFrom?.let { LocalDateTime.ofInstant(it, ZoneOffset.UTC) },
validTo = platform.validTo?.let { LocalDateTime.ofInstant(it, ZoneOffset.UTC) },
// validFrom/validTo приходят как Instant; в naive-поля БД пишем настенные часы локальной
// зоны JVM (ZoneId.systemDefault()) — naive-время в системе локальное, без UTC-семантики.
validFrom = platform.validFrom?.let { LocalDateTime.ofInstant(it, ZoneId.systemDefault()) },
validTo = platform.validTo?.let { LocalDateTime.ofInstant(it, ZoneId.systemDefault()) },
now = now
)
visibilityRefreshJobService.enqueue(spacecraftId, noradId, "NSI_SYNC")
@@ -11,7 +11,6 @@ import space.nstart.pcp_tgu_service.repository.PlanIssueAttemptRepository
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.UUID
/**
@@ -67,7 +66,7 @@ class TestPlanDecisionService(
planId = plan.planId,
attemptId = attempt.attemptId,
decision = decision,
decisionTime = LocalDateTime.now(ZoneOffset.UTC),
decisionTime = LocalDateTime.now(),
reason = reason?.takeIf { it.isNotBlank() } ?: DEFAULT_REASON
)
handlePlanDecisionUseCase.handle(message)
@@ -4,7 +4,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp_tgu_service.repository.VisibilityRefreshJobRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Coalesce-сервис для visibility_refresh_job по spacecraftId.
@@ -16,6 +15,6 @@ class VisibilityRefreshJobService(
/** Ставит или переоткрывает refresh job без дублей. */
@Transactional
fun enqueue(spacecraftId: String, noradId: Long?, reason: String) {
visibilityRefreshJobRepository.upsertPending(spacecraftId, noradId, reason, LocalDateTime.now(ZoneOffset.UTC))
visibilityRefreshJobRepository.upsertPending(spacecraftId, noradId, reason, LocalDateTime.now())
}
}
@@ -12,7 +12,6 @@ import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import space.nstart.pcp_tgu_service.repository.VisibilityRefreshJobRepository
import java.net.InetAddress
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
/**
@@ -39,7 +38,7 @@ class VisibilityRefreshWorker(
private fun claimJobs(limit: Int): List<VisibilityRefreshJobEntity> =
transactionTemplate.execute {
val now = LocalDateTime.now(ZoneOffset.UTC)
val now = LocalDateTime.now()
val jobs = visibilityRefreshJobRepository.findPendingForUpdate(now, limit)
jobs.forEach { job ->
job.status = JobStatus.RUNNING
@@ -62,10 +61,10 @@ class VisibilityRefreshWorker(
}
try {
val timeStart = LocalDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.MINUTES)
val timeStart = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES)
val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays)
val payload = externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop)
spacecraftVisibilitySnapshotService.saveSnapshot(spacecraftId, noradId, payload, LocalDateTime.now(ZoneOffset.UTC))
spacecraftVisibilitySnapshotService.saveSnapshot(spacecraftId, noradId, payload, LocalDateTime.now())
markDone(spacecraftId)
} catch (ex: Exception) {
log.warn("Visibility refresh job failed. spacecraftId={}, noradId={}", spacecraftId, noradId, ex)
@@ -80,7 +79,7 @@ class VisibilityRefreshWorker(
job.rerunRequested = false
job.lockedBy = null
job.lockedUntil = null
job.updatedAt = LocalDateTime.now(ZoneOffset.UTC)
job.updatedAt = LocalDateTime.now()
visibilityRefreshJobRepository.save(job)
}
}
@@ -91,7 +90,7 @@ class VisibilityRefreshWorker(
job.status = if (job.attempts >= MAX_ATTEMPTS) JobStatus.FAILED else JobStatus.PENDING
job.lockedBy = null
job.lockedUntil = null
job.updatedAt = LocalDateTime.now(ZoneOffset.UTC)
job.updatedAt = LocalDateTime.now()
visibilityRefreshJobRepository.save(job)
}
}
@@ -103,7 +102,7 @@ class VisibilityRefreshWorker(
job.reason = reason
job.lockedBy = null
job.lockedUntil = null
job.updatedAt = LocalDateTime.now(ZoneOffset.UTC)
job.updatedAt = LocalDateTime.now()
visibilityRefreshJobRepository.save(job)
}
}
@@ -19,9 +19,9 @@ class VisibilityPayloadParserTest {
}
@Test
fun `parses UTC instant as UTC LocalDateTime regardless of JVM default zone`() {
// planned_plan.start_time/end_time по контракту хранят UTC. Раньше парсер конвертировал момент
// через ZoneId.systemDefault(), и на не-UTC JVM (напр. МСК, +3) время уезжало на офсет.
fun `projects zoned instant into JVM default zone as naive local LocalDateTime`() {
// naive-время в системе — настенные часы локальной зоны JVM (без UTC-семантики). Парсер проецирует
// зонированный инстант через ZoneId.systemDefault(), поэтому на МСК (+3) 14:14Z даёт локальные 17:14.
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Moscow"))
val payload = """
[{
@@ -35,9 +35,9 @@ class VisibilityPayloadParserTest {
val window = parser.parse("20", payload).points.single().observationWindows.single()
// Значение совпадает с UTC-стенкой инстанта (14:14), а не со сдвигом на пояс JVM (17:14).
assertEquals(LocalDateTime.parse("2026-06-02T14:14:08"), window.startTime)
assertEquals(LocalDateTime.parse("2026-06-02T14:20:00"), window.endTime)
// Инстант 14:14Z спроецирован в локальную зону JVM (МСК, +3) — настенные 17:14.
assertEquals(LocalDateTime.parse("2026-06-02T17:14:08"), window.startTime)
assertEquals(LocalDateTime.parse("2026-06-02T17:20:00"), window.endTime)
}
@Test
@@ -24,7 +24,6 @@ import space.nstart.pcp_tgu_service.integration.api.MissionPlaningClient
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.Optional
import java.util.UUID
@@ -227,7 +226,7 @@ class CreateChainPlanUseCaseTest {
// Фиксированная база времени на инстанс теста: time() детерминирован, окно в запросе и в
// моке сырых ЗРВ совпадает байт-в-байт (без дрейфа now() между вызовами).
private val t0: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
private val t0: LocalDateTime = LocalDateTime.now()
private fun time(hour: Int): LocalDateTime = t0.plusHours(hour.toLong())
@Suppress("UNCHECKED_CAST")
@@ -23,7 +23,6 @@ import space.nstart.pcp_tgu_service.integration.api.MissionPlaningClient
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.Optional
import java.util.UUID
@@ -267,7 +266,7 @@ class CreateDraftUseCaseTest {
private fun props(freeze: Long): PlanningProperties =
PlanningProperties(30, 180, 60, false, 10, 14, 0, 30_000, 5, 30, 300, 30, freeze)
private fun time(hour: Int): LocalDateTime = LocalDateTime.now(ZoneOffset.UTC).plusHours(hour.toLong())
private fun time(hour: Int): LocalDateTime = LocalDateTime.now().plusHours(hour.toLong())
// Kotlin non-null матчеры: ArgumentMatchers.any() возвращает null и роняет NPE на non-null параметрах.
@Suppress("UNCHECKED_CAST")
@@ -19,7 +19,6 @@ import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
import space.nstart.pcp_tgu_service.integration.api.MissionPlaningClient
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.Optional
import java.util.UUID
@@ -79,8 +78,8 @@ class DeleteDraftUseCaseTest {
PlannedPlanEntity(
planId = UUID.randomUUID(),
spacecraftId = "25544",
startTime = LocalDateTime.now(ZoneOffset.UTC).plusHours(3),
endTime = LocalDateTime.now(ZoneOffset.UTC).plusHours(4),
startTime = LocalDateTime.now().plusHours(3),
endTime = LocalDateTime.now().plusHours(4),
kppId = "KPP-1",
status = status
)

Some files were not shown because too many files have changed in this diff Show More