335 lines
27 KiB
TypeScript
335 lines
27 KiB
TypeScript
import {finiteMetric} from './spatialSessionModel';
|
||
export {finiteMetric, deviceTelemetry, formatNumber} from './spatialSessionModel';
|
||
export type {K1DeviceTelemetry} from './spatialSessionModel';
|
||
import type { StatusTone } from "@nodedc/ui-react";
|
||
|
||
import type { BackendStatus } from "@mission-core/plugin-sdk";
|
||
import type {
|
||
XgridsConnectionPolicyAction,
|
||
XgridsK1Metrics,
|
||
XgridsK1State,
|
||
} from "./api";
|
||
import {
|
||
connectionPolicyAllows,
|
||
connectionPolicyDecision,
|
||
} from "./lifecycle";
|
||
|
||
const connectionPolicyReasonCopy: Record<string, string> = {
|
||
"connection-supervisor-closed": "Контур связи K1 закрыт.",
|
||
"supervisor-action-not-allowed": "Текущая связь с K1 не подтверждает право на эту физическую команду.",
|
||
"network-provision-operation-active": "Предыдущая сетевая операция K1 ещё не завершена.",
|
||
"acquisition-active": "Сетевой режим K1 нельзя менять во время активного приёма.",
|
||
"acquisition-cleanup-pending": "Локальный приём K1 ещё завершает очистку ресурсов.",
|
||
"local-runtime-active": "Локальный исполнительный контур K1 ещё активен.",
|
||
"control-session-not-admissible-for-network-change": "Текущая управляющая сессия K1 ещё не допускает смену сети.",
|
||
"network-mutation-reconciliation-required": "Результат предыдущей сетевой записи K1 не подтверждён.",
|
||
"network-mutation-ledger-corrupt": "Журнал сетевых изменений K1 повреждён.",
|
||
"fresh-ble-candidate-required": "Для этого действия нужен K1 из нового Bluetooth-поиска.",
|
||
"retained-recovery-context-unavailable": "Сохранённый Bluetooth-контекст текущего K1 больше недоступен.",
|
||
"fresh-candidate-supersedes-retained-recovery": "K1 снова виден в свежем поиске; используйте новый найденный экземпляр.",
|
||
"reconciliation-target-not-observed": "В свежем Bluetooth-поиске не найден K1, связанный с незавершённой записью.",
|
||
"reconciliation-target-not-retained": "Текущий серверный Bluetooth-контекст относится не к тому K1, для которого не завершена сетевая операция.",
|
||
"current-device-context-unavailable": "Текущий K1 не подтверждён в оперативном контексте этого процесса.",
|
||
"durable-recovery-target-unavailable": "В сохранённом серверном состоянии нет одной точной пары K1 и режима для проверки после перезапуска.",
|
||
"fresh-candidate-supersedes-durable-recovery": "Нужный K1 снова найден свежим Bluetooth-поиском; сервер требует проверить именно свежий экземпляр.",
|
||
"retained-recovery-supersedes-durable-recovery": "Сервер хранит более свежий контекст текущего K1.",
|
||
"configured-endpoint-unavailable": "Нет подтверждённого сохранённого адреса K1 для безопасной проверки.",
|
||
"configured-endpoint-probe-lifecycle-busy": "Контур подключения K1 занят другой операцией.",
|
||
"configured-endpoint-changed-during-probe": "Адрес K1 изменился во время проверки; результат отброшен.",
|
||
"configured-endpoint-probe-failed": "Маршрут и управляющий endpoint K1 не удалось проверить.",
|
||
"host-path-unavailable": "На этом компьютере не подтверждён сетевой путь до K1.",
|
||
"host-route-not-direct": "Маршрут до K1 проходит не через ожидаемую локальную сеть.",
|
||
"endpoint-not-reachable": "Управляющий endpoint K1 сейчас недоступен.",
|
||
"device-identity-unverified": "Идентичность подключённого K1 ещё не подтверждена.",
|
||
"device-identity-stale": "Подтверждение идентичности K1 устарело.",
|
||
"device-identity-mismatch": "Подключённое устройство не совпало с выбранным K1.",
|
||
"device-identity-pin-store-corrupt": "Хранилище привязки устройства повреждено.",
|
||
"network-provisioning-idempotency-unavailable": "Журнал сетевых намерений K1 недоступен.",
|
||
"network-provisioning-idempotency-corrupt": "Журнал сетевых намерений K1 повреждён.",
|
||
"network-provisioning-idempotency-invalid": "Журнал сетевых намерений K1 не прошёл проверку.",
|
||
"network-provisioning-idempotency-operation-mismatch": "Текущая сетевая операция не совпала с сохранённым намерением.",
|
||
"semantic-topology-store-corrupt": "Сохранённая топология K1 повреждена.",
|
||
"control-plane-not-healthy": "Управляющий канал K1 не подтверждён.",
|
||
"connection-lease-not-reachable": "Текущая сессия связи K1 больше не подтверждена.",
|
||
"data-plane-stalled": "Поток данных K1 перестал обновляться.",
|
||
"data-plane-lost": "Поток данных K1 потерян.",
|
||
"physical-command-reconciliation-required": "Результат предыдущей физической команды K1 не подтверждён.",
|
||
"physical-device-already-active": "Последнее подтверждённое состояние K1 — активное сканирование.",
|
||
"physical-command-recovery-target-unavailable": "Журнал не содержит точную Bluetooth-цель для восстановления K1.",
|
||
"physical-command-recovery-target-not-observed": "Исходный K1 пока не найден в свежем Bluetooth-поиске.",
|
||
"physical-command-recovery-target-not-retained": "Сохранённый Bluetooth-контекст относится не к исходному K1.",
|
||
"physical-command-recovery-target-mismatch": "Выбрано другое устройство или другой способ связи, чем в незавершённой физической сессии.",
|
||
"physical-command-ledger-corrupt": "Журнал физических команд K1 повреждён.",
|
||
"physical-command-ledger-unavailable": "Журнал физических команд K1 недоступен.",
|
||
"physical-control-authority-unavailable": "Управляющая связь не позволяет безопасно отправить физический STOP.",
|
||
"ble-runtime-restart-required": "BLE-контур требует контролируемого перезапуска.",
|
||
"ble-runtime-cleanup-pending": "BLE-контур завершает предыдущую операцию.",
|
||
"ble-runtime-busy": "BLE-контур занят другой операцией.",
|
||
"k1-lifecycle-process-lease-network-owned": "Сетевой переход K1 ещё владеет исполнительным контуром.",
|
||
"k1-lifecycle-process-lease-control-owned": "Управляющая сессия K1 ещё владеет исполнительным контуром.",
|
||
"local-acquisition-receiver-not-active": "Активного локального приёмника сейчас нет.",
|
||
"physical-stop-is-authoritative": "Доступна подтверждённая физическая остановка K1; локальная очистка не должна её подменять.",
|
||
"action-not-implemented": "Это действие не реализовано и не может быть выполнено.",
|
||
"connection-reconfiguration-lifecycle-busy": "Другое действие подключения ещё не завершено.",
|
||
"connection-reconfiguration-bridge-only": "Это действие доступно только для подключения Bridge.",
|
||
"connection-reconfiguration-current-device-unavailable": "Нет точной привязки текущего устройства для изменения сети.",
|
||
"connection-reconfiguration-active": "Сначала завершите или отмените текущий выбор устройства или сети.",
|
||
"connection-reconfiguration-required-device-not-observed": "Исходное устройство не найдено в текущем Bluetooth-поиске.",
|
||
"connection-reconfiguration-not-active": "Активного изменения устройства или сети уже нет.",
|
||
"connection-reconfiguration-process-lease-busy": "Другой локальный процесс ещё управляет подключением устройства.",
|
||
"connection-reconfiguration-acquisition-changed": "Состояние приёма изменилось во время подготовки подключения.",
|
||
"connection-reconfiguration-revision-conflict": "Выбор устройства или сети уже изменился в другой вкладке.",
|
||
"connection-reconfiguration-binding-conflict": "Активное подключение изменилось до выполнения действия.",
|
||
"connection-reconfiguration-fresh-scan-required": "Для этого действия нужен новый Bluetooth-поиск.",
|
||
"connection-reconfiguration-discovery-conflict": "Результаты Bluetooth-поиска относятся к предыдущему действию.",
|
||
"connection-reconfiguration-target-mismatch": "Изменение сети разрешено только для исходного устройства.",
|
||
"acquisition-start-operation-active": "Запуск приёма ещё не завершён.",
|
||
"control-session-state-unsafe": "Управляющий диалог ещё не достиг безопасного состояния ожидания.",
|
||
};
|
||
|
||
const connectionPolicyNextActionCopy: Record<string, string> = {
|
||
"wait-for-operation": "Дождитесь завершения текущей операции и обновите состояние.",
|
||
"diagnose-network-ledger": "Не отправляйте новые команды и проверьте журнал сетевой операции.",
|
||
"diagnose-physical-command-ledger": "Не повторяйте команду; сначала проверьте журнал физических команд.",
|
||
"restart-ble-runtime": "Контролируемо перезапустите локальный BLE-контур и обновите состояние.",
|
||
"scan-ble": "Выполните свежий поиск Bluetooth-устройств.",
|
||
"observe-fresh-device-network": "Нажмите «Переподключиться», чтобы проверить состояние выбранного K1.",
|
||
"observe-current-device-network": "Нажмите «Переподключиться», чтобы проверить связь с тем же K1.",
|
||
"observe-configured-device-network": "Нажмите «Переподключиться», чтобы проверить сохранённое подключение K1.",
|
||
"recover-current-device-network": "Нажмите «Подключиться заново».",
|
||
"inspect-host-network": "Проверьте активную локальную сеть и маршрут этого компьютера.",
|
||
"probe-endpoint": "Дождитесь обновления подключения K1.",
|
||
"verify-control-device-info": "Подключитесь заново к выбранному K1.",
|
||
"select-connection-intent": "Выберите способ подключения и заново подтвердите текущий K1.",
|
||
"start-acquisition": "Повторно откройте финальное подтверждение физического START.",
|
||
"stop-acquisition": "Остановите K1 через подтверждённую физическую остановку.",
|
||
"stop-local-receiver": "Завершите локальный приём; физическое состояние K1 проверьте вручную.",
|
||
"retire-unavailable-physical-target": "Явно исключите недоступный прежний K1 перед новым выбором.",
|
||
"manual-recovery-required": "Автоматически безопасного продолжения нет; проверьте состояние K1 вручную.",
|
||
"cancel-reconfiguration": "Отмените текущий выбор и вернитесь к обычному восстановлению подключения.",
|
||
};
|
||
|
||
export interface ConnectionPolicyOperatorGuidance {
|
||
reason: string;
|
||
nextAction: string;
|
||
}
|
||
|
||
const connectionModeSelectionReasonCopy: Record<string, string> = {
|
||
"connection-mode-selection-physical-state-unsafe":
|
||
"Предыдущая физическая команда K1 осталась без подтверждённого результата. Поэтому способ подключения пока нельзя изменить.",
|
||
"connection-mode-selection-control-state-unsafe":
|
||
"Текущий управляющий процесс K1 ещё не завершён. После его завершения способ подключения снова станет доступен.",
|
||
"connection-mode-selection-lifecycle-busy":
|
||
"Текущее действие подключения ещё завершается. После него способ подключения снова станет доступен.",
|
||
"connection-reconfiguration-active":
|
||
"Сначала завершите или отмените текущий выбор устройства или сети.",
|
||
};
|
||
|
||
const physicalRetirementReasonCopy: Record<string, string> = {
|
||
"physical-command-retirement-operation-conflict":
|
||
"Сейчас завершается другая операция с физическим состоянием устройства. Дождитесь её завершения и обновите состояние.",
|
||
"physical-command-retirement-state-unsafe":
|
||
"Состояние предыдущей физической команды изменилось. Обновите состояние перед новым выбором устройства.",
|
||
"physical-command-retirement-not-required":
|
||
"Предыдущая физическая команда уже разрешена или больше не удерживает выбор устройства. Обновите состояние и продолжите обычное подключение.",
|
||
"physical-command-target-retired":
|
||
"Предыдущее устройство уже выведено из текущего контура. Можно сразу выполнить новый явный Bluetooth-поиск.",
|
||
"network-provision-operation-active":
|
||
"Сейчас завершается подключение устройства к сети. Дождитесь результата перед выбором другого устройства.",
|
||
"connection-reconfiguration-active":
|
||
"Сначала завершите или отмените текущее изменение устройства или сети.",
|
||
"control-local-retirement-pending":
|
||
"Управляющая сессия ещё освобождает локальные ресурсы. Дождитесь завершения и обновите состояние.",
|
||
"acquisition-active":
|
||
"Сканирование ещё активно. Сначала остановите его и дождитесь подтверждённого завершения.",
|
||
"acquisition-cleanup-pending":
|
||
"Локальный приём ещё освобождает ресурсы после остановки. Дождитесь завершения.",
|
||
"acquisition-start-operation-active":
|
||
"Запуск сканирования ещё не завершён. Дождитесь его результата перед сменой устройства.",
|
||
"acquisition-stop-operation-active":
|
||
"Остановка сканирования ещё не завершена. Дождитесь её результата перед сменой устройства.",
|
||
"local-runtime-active":
|
||
"Локальный поток устройства ещё активен или завершается. Дождитесь перехода в состояние ожидания.",
|
||
"control-session-state-unsafe":
|
||
"Управляющая сессия устройства ещё не завершена. Дождитесь её закрытия и обновите состояние.",
|
||
"ble-runtime-busy":
|
||
"Bluetooth занят другой операцией устройства. Дождитесь её завершения; новый поиск автоматически не запустится.",
|
||
"ble-runtime-cleanup-pending":
|
||
"Bluetooth ещё завершает предыдущую операцию. Дождитесь освобождения соединения и обновите состояние.",
|
||
"ble-runtime-restart-required":
|
||
"Локальный Bluetooth-контур требует контролируемого перезапуска. Команды устройству не отправлялись.",
|
||
"k1-lifecycle-process-lease-active":
|
||
"Другой локальный процесс ещё завершает действие с устройством. Дождитесь его завершения и обновите состояние.",
|
||
"physical-command-ledger-corrupt":
|
||
"Журнал физической команды повреждён. Не выбирайте другое устройство до проверки журнала.",
|
||
};
|
||
|
||
const physicalReopenReasonCopy: Record<string, string> = {
|
||
...physicalRetirementReasonCopy,
|
||
"physical-command-reconciliation-reopen-not-required":
|
||
"Это устройство больше не требует возврата из предыдущего выбора. Обновите состояние и продолжите обычное подключение.",
|
||
"physical-command-reconciliation-reopen-target-not-observed":
|
||
"Предыдущее устройство не найдено в последнем Bluetooth-поиске. Обновите поиск после проверки питания K1.",
|
||
"physical-command-reconciliation-reopen-target-not-connectable":
|
||
"Предыдущее устройство найдено, но сейчас не принимает Bluetooth-подключение. Проверьте питание K1 и повторите явный поиск.",
|
||
"physical-command-reconciliation-reopen-candidate-ambiguous":
|
||
"Последний Bluetooth-поиск не подтвердил один точный экземпляр предыдущего K1. Повторите поиск рядом только с нужным устройством.",
|
||
"physical-command-reconciliation-reopen-operation-conflict":
|
||
"Другая операция уже меняет локальное состояние предыдущего устройства. Дождитесь её завершения и обновите поиск.",
|
||
"device-calibration-read-active":
|
||
"Сейчас читается калибровка устройства. Дождитесь завершения проверки перед повторным использованием K1.",
|
||
};
|
||
|
||
/** Human explanation for a backend-disabled topology selector. */
|
||
export function connectionModeSelectionGuidance(
|
||
state: XgridsK1State | null | undefined,
|
||
): string | null {
|
||
const selection = state?.connection_lifecycle?.mode_selection;
|
||
if (!selection) return null;
|
||
if (selection?.allowed === true) return null;
|
||
const reasonCode = selection?.reason_codes.find((code) => code.trim().length > 0);
|
||
return reasonCode
|
||
? connectionModeSelectionReasonCopy[reasonCode]
|
||
?? "Способ подключения пока недоступен, потому что состояние K1 не позволяет безопасно изменить его."
|
||
: "Способ подключения пока недоступен, потому что состояние K1 не позволяет безопасно изменить его."
|
||
}
|
||
|
||
/** Human explanation for a currently unavailable local-only device escape. */
|
||
export function physicalRetirementGuidance(
|
||
state: XgridsK1State | null | undefined,
|
||
): string | null {
|
||
const retirement = (
|
||
state?.physical_command
|
||
?? state?.application_control_session?.physical_command
|
||
?? null
|
||
)?.operator_retirement;
|
||
if (!retirement || retirement.allowed === true) return null;
|
||
const reasonCode = retirement.reason_codes.find((code) => code.trim().length > 0);
|
||
return reasonCode
|
||
? physicalRetirementReasonCopy[reasonCode]
|
||
?? "Выбор другого устройства пока небезопасен. Обновите состояние после завершения текущей операции."
|
||
: "Выбор другого устройства пока небезопасен. Обновите состояние после завершения текущей операции.";
|
||
}
|
||
|
||
/** Human explanation for a backend-disabled exact retired-device reopen. */
|
||
export function physicalReopenGuidance(
|
||
state: XgridsK1State | null | undefined,
|
||
): string | null {
|
||
const reopen = state?.physical_command?.operator_reconciliation_reopen;
|
||
if (!reopen || reopen.allowed === true) return null;
|
||
const reasonCode = reopen.reason_codes.find((code) => code.trim().length > 0);
|
||
return reasonCode
|
||
? physicalReopenReasonCopy[reasonCode]
|
||
?? "Повторная проверка предыдущего K1 сейчас небезопасна. Дождитесь завершения текущей операции и обновите поиск."
|
||
: "Повторная проверка предыдущего K1 сейчас небезопасна. Обновите поиск после завершения текущей операции.";
|
||
}
|
||
|
||
export function connectionPolicyOperatorGuidance(
|
||
state: XgridsK1State | null | undefined,
|
||
action: XgridsConnectionPolicyAction,
|
||
): ConnectionPolicyOperatorGuidance | null {
|
||
if (connectionPolicyAllows(state, action)) return null;
|
||
const decision = connectionPolicyDecision(state, action);
|
||
const reasonCode = decision?.reason_codes.find((code) => code.trim().length > 0);
|
||
const recommendedAction = state?.connection_policy?.recommended_action?.trim();
|
||
return {
|
||
reason: reasonCode
|
||
? connectionPolicyReasonCopy[reasonCode]
|
||
?? "Система временно запретила действие до восстановления подтверждённого состояния."
|
||
: "Подтверждённая политика действия ещё не получена.",
|
||
nextAction: recommendedAction
|
||
? connectionPolicyNextActionCopy[recommendedAction]
|
||
?? "Обновите состояние подключения и следуйте рекомендованному безопасному действию."
|
||
: "Обновите состояние подключения перед новым действием.",
|
||
};
|
||
}
|
||
|
||
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 (["live", "replay"].includes(phase)) return "success";
|
||
if (phase === "connected") return "warning";
|
||
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 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;
|
||
}
|
||
|
||
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 sourceModeLabel(mode: string | null | undefined): string {
|
||
if (mode === "live") return "Реальное время";
|
||
if (mode === "replay") return "Повтор записи";
|
||
if (mode === "idle") return "Ожидание";
|
||
return "Неизвестно";
|
||
}
|