132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
import type { StatusTone } from "@nodedc/ui-react";
|
||
|
||
import type { BackendStatus } from "@mission-core/plugin-sdk";
|
||
import type { XgridsK1Metrics } from "./api";
|
||
|
||
const phaseLabels: Record<string, string> = {
|
||
idle: "Ожидание",
|
||
scanning: "Поиск Bluetooth",
|
||
device_selected: "Устройство выбрано",
|
||
provisioning: "Передача настроек Wi‑Fi",
|
||
connecting: "Подключение",
|
||
connected: "Устройство подключено",
|
||
starting_live: "Запуск потока",
|
||
live: "Поток в реальном времени",
|
||
replay: "Повтор записи",
|
||
stopping: "Остановка",
|
||
error: "Ошибка",
|
||
};
|
||
|
||
export function phaseLabel(phase: string | null | undefined): string {
|
||
if (!phase) return "Нет состояния";
|
||
return phaseLabels[phase] ?? "Неизвестное состояние";
|
||
}
|
||
|
||
export function phaseTone(phase: string | null | undefined): StatusTone {
|
||
if (!phase) return "neutral";
|
||
if (phase === "error") return "danger";
|
||
if (["connected", "live", "replay"].includes(phase)) return "success";
|
||
if (["scanning", "provisioning", "connecting", "starting_live", "stopping"].includes(phase)) return "accent";
|
||
return "neutral";
|
||
}
|
||
|
||
export function backendLabel(status: BackendStatus): string {
|
||
return {
|
||
unconfigured: "Модель не выбрана",
|
||
checking: "Проверка контура",
|
||
online: "Контур доступен",
|
||
degraded: "Контур ограничен",
|
||
offline: "Контур недоступен",
|
||
}[status];
|
||
}
|
||
|
||
export function backendTone(status: BackendStatus): StatusTone {
|
||
if (status === "online") return "success";
|
||
if (status === "degraded" || status === "checking") return "warning";
|
||
if (status === "unconfigured") return "neutral";
|
||
return "danger";
|
||
}
|
||
|
||
export function eventStatusLabel(status: string): string {
|
||
return {
|
||
connecting: "подключение",
|
||
open: "подключён",
|
||
closed: "закрыт",
|
||
error: "ошибка",
|
||
}[status] ?? "неизвестно";
|
||
}
|
||
|
||
export function finiteMetric(value: number | null | undefined): number | null {
|
||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||
}
|
||
|
||
export function pipelineLatency(metrics: XgridsK1Metrics | undefined): number | null {
|
||
if (!metrics) return null;
|
||
const direct = finiteMetric(metrics.pipeline_ms ?? metrics.end_to_end_ms);
|
||
if (direct !== null) return direct;
|
||
const segments = [finiteMetric(metrics.mqtt_to_decode_ms), finiteMetric(metrics.publish_ms)]
|
||
.filter((value): value is number => value !== null);
|
||
return segments.length === 2 ? segments.reduce((total, value) => total + value, 0) : null;
|
||
}
|
||
|
||
function nonNegativeMetric(value: number | null | undefined): number | null {
|
||
const finite = finiteMetric(value);
|
||
return finite !== null && finite >= 0 ? finite : null;
|
||
}
|
||
|
||
export interface K1DeviceTelemetry {
|
||
elapsedSeconds: number | null;
|
||
routeDistanceMeters: number | null;
|
||
speedMetersPerSecond: number | null;
|
||
}
|
||
|
||
export interface SpatialActionFailure {
|
||
title: string;
|
||
detail: string;
|
||
}
|
||
|
||
export function spatialActionFailure(
|
||
error: string | null | undefined,
|
||
): SpatialActionFailure | null {
|
||
const detail = error?.trim();
|
||
return detail
|
||
? {
|
||
title: "Действие K1 не выполнено",
|
||
detail,
|
||
}
|
||
: null;
|
||
}
|
||
|
||
export function deviceTelemetry(
|
||
metrics: XgridsK1Metrics | null | undefined,
|
||
): K1DeviceTelemetry {
|
||
return {
|
||
elapsedSeconds: nonNegativeMetric(
|
||
metrics?.device_elapsed_seconds ?? metrics?.elapsed_seconds,
|
||
),
|
||
routeDistanceMeters: nonNegativeMetric(
|
||
metrics?.device_route_distance_meters ?? metrics?.route_distance_meters,
|
||
),
|
||
speedMetersPerSecond: nonNegativeMetric(
|
||
metrics?.device_speed_meters_per_second ??
|
||
metrics?.device_speed_mps ??
|
||
metrics?.speed_meters_per_second,
|
||
),
|
||
};
|
||
}
|
||
|
||
export function formatNumber(value: number | null, digits = 1): string {
|
||
if (value === null) return "—";
|
||
return value.toLocaleString("ru-RU", {
|
||
maximumFractionDigits: digits,
|
||
minimumFractionDigits: digits,
|
||
});
|
||
}
|
||
|
||
export function sourceModeLabel(mode: string | null | undefined): string {
|
||
if (mode === "live") return "Реальное время";
|
||
if (mode === "replay") return "Повтор записи";
|
||
if (mode === "idle") return "Ожидание";
|
||
return "Неизвестно";
|
||
}
|