Files
dc-observatio/services/pcp-tgu-ui-service/src/api/tguApi.ts
T

189 lines
6.1 KiB
TypeScript

import {
TguApiError,
type TguPlan,
type TguPlanDecision,
type TguPlanDecisionMessage,
type TguPlatform,
} from '../model/tguTypes'
import { toLocalIso } from '../utils/localTime'
import { apiFetch } from './apiFetch'
export type FlightLinePoint = {
time: string
revolution: number
lat: number
long: number
latLeft: number
longLeft: number
latInnerLeft: number
longInnerLeft: number
latInnerRight: number
longInnerRight: number
latRight: number
longRight: number
revSign: 'ASC' | 'DESC'
}
/**
* Трасса полёта КА (flight-line) на интервале для отрисовки на карте/таймлайне.
* @param noradId NORAD-идентификатор КА.
* @param fromMs Начало интервала, мс от эпохи.
* @param toMs Конец интервала, мс от эпохи.
*
* @returns Точки трассы полёта на интервале.
*/
export async function fetchFlightLine(
noradId: string,
fromMs: number,
toMs: number,
): Promise<FlightLinePoint[]> {
const query = new URLSearchParams({
time_start: toLocalIso(fromMs),
time_stop: toLocalIso(toMs),
})
return fetchJson<FlightLinePoint[]>(
`/api/tgu-planning/spacecraft/${encodeURIComponent(noradId)}/flight-line?${query}`,
)
}
/**
* Список платформ (КА) ТГУ.
* @returns Платформы планирования ТГУ.
*/
export async function fetchPlatforms(): Promise<TguPlatform[]> {
return fetchJson<TguPlatform[]>('/api/tgu-planning/platforms')
}
/**
* Планы ТГУ с заданной глубиной истории, опционально по одному КА.
* @param historyDays Глубина истории в сутках.
* @param spacecraftId Идентификатор КА для фильтра (необязателен — иначе по всем КА).
*
* @returns Планы ТГУ.
*/
export async function fetchPlans(historyDays: number, spacecraftId?: string): Promise<TguPlan[]> {
const query = new URLSearchParams({ historyDays: String(historyDays) })
const path = spacecraftId
? `/api/tgu-planning/spacecraft/${encodeURIComponent(spacecraftId)}/plans?${query}`
: `/api/tgu-planning/plans?${query}`
return fetchJson<TguPlan[]>(path)
}
/**
* Отправляет решение оператора по плану (принять/отклонить и т.п.).
* @param planId Идентификатор плана.
* @param decision Решение по плану.
* @param reason Причина решения (необязательна).
*
* @returns Сообщение-результат обработки решения.
*/
export async function sendPlanDecision(
planId: string,
decision: TguPlanDecision,
reason?: string,
): Promise<TguPlanDecisionMessage> {
const query = new URLSearchParams({ decision })
if (reason?.trim()) {
query.set('reason', reason.trim())
}
return fetchJson<TguPlanDecisionMessage>(
`/api/tgu-planning/plans/${encodeURIComponent(planId)}/decision?${query}`,
{ method: 'POST' },
true,
)
}
/**
* «Снять с учёта» терминально-негативный план: он перестаёт считаться проблемой и красить
* индикатор КА. Статус и цепочку не трогает; обратимо ([restorePlan]). 422 — статус не снимаемый.
* @param planId Идентификатор плана.
*/
export async function dismissPlan(planId: string): Promise<void> {
await fetchJson<void>(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/dismiss`, {
method: 'POST',
})
}
/**
* «Вернуть на учёт» ранее снятый план.
* @param planId Идентификатор плана.
*/
export async function restorePlan(planId: string): Promise<void> {
await fetchJson<void>(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/restore`, {
method: 'POST',
})
}
/**
* ТЕСТ: подтверждает закладку плана (AWAITING_LAYIN → LAID_IN) через тест-контроллер ТГУ.
* decisionRequest=false — пробрасываем реальный текст ошибки бэка (напр. гард «закладка ±10 мин»).
* @param planId Идентификатор плана.
*/
export async function confirmPlanLayin(planId: string): Promise<void> {
await fetchJson<void>(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/confirm-layin`, {
method: 'POST',
})
}
async function fetchJson<T>(url: string, init?: RequestInit, decisionRequest = false): Promise<T> {
let response: Response
try {
response = await apiFetch(url, {
headers: { Accept: 'application/json' },
...init,
})
} catch {
throw new TguApiError('Не удалось подключиться к backend API.', 'network')
}
const text = await response.text()
if (!response.ok) {
throw new TguApiError(
errorMessage(response.status, text, decisionRequest),
'http',
response.status,
)
}
if (!text.trim()) {
return undefined as T
}
return JSON.parse(text) as T
}
function errorMessage(status: number, body: string, decisionRequest: boolean): string {
if (decisionRequest && status === 404) {
return 'План не найден.'
}
if (decisionRequest && status === 409) {
return 'План не является активным планом, ожидающим решения, или для него нет active attempt.'
}
if (decisionRequest && status >= 500) {
return 'Ошибка сервиса при отправке решения.'
}
return extractErrorText(body) || 'Ошибка backend API.'
}
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 ['reason', 'message', 'error', 'detail']) {
const value = parsed[field]
if (typeof value === 'string' && value.trim()) {
return value
}
}
} catch {
return trimmed
}
return trimmed
}