Новый сервис pcp-tgu-ui-service: ТГУ-фронт на headless-auth nstart + платформенный CI/деплой

This commit is contained in:
Дмитрий Соловьев
2026-06-09 13:35:13 +03:00
parent 0b617594fd
commit 505bf1f31c
129 changed files with 21640 additions and 0 deletions
@@ -0,0 +1,202 @@
import { TguApiError } from '../model/tguTypes'
import { toLocalIso } from '../utils/localTime'
import { apiFetch } from './apiFetch'
const BASE = '/api/pcp-ballistics'
export type PointVisibilityParam = {
/** NORAD-идентификатор КА. */
noradId: number
/** Идентификатор объекта (цели) из запроса. */
objectId: string
/** Момент видимости (локальное время, ISO без зоны). */
time: string
/** Номер витка. */
revolution: number
/** Крен (gamma), град. */
gamma: number
/** Угол Солнца, град. */
sunAngle: number
/** Наклонная дальность. */
range: number
/** Направление витка. */
revSign: 'ASC' | 'DESC'
}
export type PointVisibilityFilters = {
/** Минимальный угол Солнца, град. */
sunAngleMin?: number
/** Минимальный крен, град. */
rollMin?: number
/** Максимальный крен, град. */
rollMax?: number
}
export type GeoPointTarget = {
id?: string
lat: number
lon: number
height?: number
}
/**
* Параметры видимости точки на заданном интервале времени из pcp-ballistics-service
* (POST /api/obj-view/mpl-point). Возвращает по одному моменту видимости на виток.
* @param noradId NORAD-идентификатор КА.
* @param target Цель (координаты точки) для расчёта видимости.
* @param fromMs Начало интервала, мс от эпохи.
* @param toMs Конец интервала, мс от эпохи.
* @param filters Фильтры по углу Солнца и крену (необязательны).
*
* @returns По одному моменту видимости на виток на интервале.
*/
export async function fetchPointVisibility(
noradId: number,
target: GeoPointTarget,
fromMs: number,
toMs: number,
filters: PointVisibilityFilters = {},
): Promise<PointVisibilityParam[]> {
const body = {
satellites: [noradId],
timeStart: toLocalDateTime(fromMs),
timeStop: toLocalDateTime(toMs),
obj: {
id: target.id ?? '',
position: {
lat: target.lat,
long: target.lon,
height: target.height ?? 0,
},
},
sunAngleMin: filters.sunAngleMin ?? null,
rollMin: filters.rollMin ?? null,
rollMax: filters.rollMax ?? null,
}
let response: Response
try {
response = await apiFetch(`${BASE}/api/obj-view/mpl-point`, {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
} catch {
throw new TguApiError('Не удалось подключиться к сервису баллистики.', 'network')
}
const text = await response.text()
if (!response.ok) {
throw new TguApiError(
extractErrorText(text) || 'Ошибка сервиса баллистики.',
'http',
response.status,
)
}
if (!text.trim()) {
return []
}
return JSON.parse(text) as PointVisibilityParam[]
}
/** Целеуказание (момент/угол места/азимут/дальность) границы зоны радиовидимости. */
export type RvaTargetPosition = {
/** Момент (локальное время, ISO без зоны). */
time: string
/** Угол места, град. */
elevation: number
/** Азимут, град. */
azimuth: number
/** Наклонная дальность. */
range: number
}
/** Зона радиовидимости (ЗРВ) станции для КА на витке. */
export type RvaZone = {
/** NORAD-идентификатор КА. */
noradId: number
/** Номер станции приёма (совпадает с number из /api/stations). */
stationId: number
/** Номер витка. */
revolution: number
/** Вход в зону. */
onStart: RvaTargetPosition
/** Максимум (кульминация). */
onMaximum: RvaTargetPosition
/** Выход из зоны. */
onStop: RvaTargetPosition
/** Длительность зоны, секунды. */
duration: number
}
/**
* Зоны радиовидимости (ЗРВ) для КА на заданном интервале из pcp-ballistics-service
* (GET /api/satellites/{norad}/rva). Баллистика считает зоны по реальным станциям приёма
* (каталог берёт у сервиса станций); каждая зона помечена номером станции (stationId).
* @param noradId NORAD-идентификатор КА.
* @param fromMs Начало интервала, мс от эпохи.
* @param toMs Конец интервала, мс от эпохи.
*
* @returns Зоны радиовидимости КА на интервале (помечены номером станции).
*/
export async function fetchSatelliteRva(
noradId: number,
fromMs: number,
toMs: number,
): Promise<RvaZone[]> {
const params = new URLSearchParams({
time_start: toLocalDateTime(fromMs),
time_stop: toLocalDateTime(toMs),
})
const url = `${BASE}/api/satellites/${noradId}/rva?${params.toString()}`
let response: Response
try {
response = await apiFetch(url, { headers: { Accept: 'application/json' } })
} catch {
throw new TguApiError('Не удалось подключиться к сервису баллистики.', 'network')
}
const text = await response.text()
if (!response.ok) {
throw new TguApiError(
extractErrorText(text) || 'Ошибка сервиса баллистики.',
'http',
response.status,
)
}
if (!text.trim()) {
return []
}
return JSON.parse(text) as RvaZone[]
}
/**
* LocalDateTime без зоны (локальные настенные часы): YYYY-MM-DDTHH:mm:ss.
* @param ms Время в миллисекундах от эпохи.
*
* @returns Строка LocalDateTime без зоны.
*/
function toLocalDateTime(ms: number): string {
return toLocalIso(ms)
}
function extractErrorText(body: string): string {
const trimmed = body.trim()
if (!trimmed) return ''
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>
for (const field of ['message', 'error', 'reason', 'detail']) {
const value = parsed[field]
if (typeof value === 'string' && value.trim()) {
return value
}
}
} catch {
return trimmed
}
return trimmed
}