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:
Executable
+75
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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`, либо пропуски явно объяснены.
|
||||
|
||||
|
||||
@@ -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 (скриншот сохранен)
|
||||
+5
-5
@@ -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"
|
||||
|
||||
+22
@@ -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))
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+2
-3
@@ -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")
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
+1
-2
@@ -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)
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+3
-4
@@ -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
|
||||
}
|
||||
|
||||
+6
-7
@@ -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(
|
||||
|
||||
+3
-4
@@ -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,
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
+3
-4
@@ -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,
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
-1
@@ -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
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+5
-6
@@ -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)
|
||||
}
|
||||
|
||||
+2
-3
@@ -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 фабрика геометрии
|
||||
|
||||
+4
-4
@@ -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
|
||||
)
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+3
-4
@@ -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,
|
||||
|
||||
+1
-2
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -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)
|
||||
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
+1
-2
@@ -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(
|
||||
|
||||
+4
-5
@@ -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(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+6
-7
@@ -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),
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+11
-11
@@ -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",
|
||||
|
||||
+3
-4
@@ -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 {
|
||||
|
||||
@@ -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
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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())
|
||||
|
||||
+2
-3
@@ -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)
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+1
-2
@@ -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())
|
||||
|
||||
+1
-2
@@ -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 = "")
|
||||
|
||||
+2
-3
@@ -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)
|
||||
|
||||
+2
-3
@@ -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)
|
||||
|
||||
+2
-3
@@ -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 = "")
|
||||
|
||||
+1
-2
@@ -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()
|
||||
|
||||
+6
-5
@@ -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)
|
||||
|
||||
+1
-2
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+1
-2
@@ -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")
|
||||
|
||||
+1
-2
@@ -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-строку.
|
||||
|
||||
+5
-6
@@ -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)
|
||||
|
||||
+1
-2
@@ -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
|
||||
}
|
||||
|
||||
+5
-6
@@ -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
|
||||
|
||||
+1
-2
@@ -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"
|
||||
|
||||
+1
-2
@@ -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(
|
||||
|
||||
+6
-6
@@ -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")
|
||||
|
||||
+1
-2
@@ -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)
|
||||
|
||||
+1
-2
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -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
|
||||
|
||||
+1
-2
@@ -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")
|
||||
|
||||
+1
-2
@@ -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")
|
||||
|
||||
+2
-3
@@ -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
Reference in New Issue
Block a user