Files
NODEDC_MISSION_CORE/plugins/xgrids-k1/frontend/src/presentation.ts
T
DCCONSTRUCTIONS 0ca7316a24 wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
2026-08-14 14:57:50 +03:00

371 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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 "Неизвестно";
}