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 = { "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 = { "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 = { "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 = { "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 = { ...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 = { 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 "Неизвестно"; }