import { useEffect, useRef, useState } from "react"; import { StatusBadge, type StatusTone } from "@nodedc/ui-react"; import type { DevicePluginConnectionProps } from "@mission-core/plugin-sdk"; import { activeStreamRecoveryPresentation, suppressGenericErrorDuringActiveStreamRecovery, } from "./activeStreamRecovery"; import { K1AcquisitionPipeline } from "./components/K1AcquisitionPipeline"; import { K1Diagnostics } from "./components/K1Diagnostics"; import { K1Metrics } from "./components/K1Metrics"; import { K1OperatorError } from "./components/K1OperatorError"; import { K1ProvisioningPipeline, unavailablePhysicalRetirementAuthority, } from "./components/K1ProvisioningPipeline"; import { backendConnectionTopology, connectionAttemptForRuntimeError, hasControlAuthority, isConnectionControlBootstrapSettling, isConfirmedLiveState, isPhysicalStopRecoverySettling, isRecoveredPhysicalScanning, isReleasedTerminalAcquisitionFailure, isSourceRuntimeBusy, readOnlyConnectionObservationTarget, recoverableAcquisition, requiresCanonicalStopAfterTerminalLocalFailure, requiresReadOnlyPhysicalRecovery, savedBridgeRequiresNetworkSetup, sourceStatusLabel, } from "./lifecycle"; import { phaseLabel, phaseTone } from "./presentation"; import { useXgridsK1Controller, type XgridsK1Controller, } from "./runtimeContext"; import type { PendingAction } from "./useXgridsK1Runtime"; import type { XgridsK1State } from "./api"; import { DEFAULT_CONNECTION_MODE, type ConnectionMode, } from "./configuration"; export { K1OperatorError }; export function shouldRenderK1GenericRuntimeError( error: string | null | undefined, hasCorrelatedConnectionAttempt: boolean, state: XgridsK1State | null | undefined, errorAction?: string | null, ): boolean { return Boolean( error && !hasCorrelatedConnectionAttempt && !suppressGenericErrorDuringActiveStreamRecovery(state, errorAction), ); } export function physicalRecoveryConnectionDetail( state: XgridsK1State | null | undefined, ): string | null { if (!requiresReadOnlyPhysicalRecovery(state)) return null; if (savedBridgeRequiresNetworkSetup(state)) { return "K1 ответил по Bluetooth, но не подключён к сохранённой общей сети. Настройки устройства не изменялись; подключите K1 к общей сети заново."; } const retirementAvailable = Boolean( unavailablePhysicalRetirementAuthority(state), ); const readOnlyVerificationAvailable = Boolean( readOnlyConnectionObservationTarget(state)?.serverBound, ); if (retirementAvailable && readOnlyVerificationAvailable) { return "Если прежний K1 снова доступен, проверьте его состояние без изменений: проверка читает состояние и не отправляет START, STOP или настройки сети. Если K1 недоступен постоянно или заменён, его можно локально исключить без связи с устройством."; } if (readOnlyVerificationAvailable) { return "Проверьте состояние прежнего K1 без изменений устройства. Проверка использует сохранённое системой подключение и не отправляет START, STOP или настройки сети."; } if (retirementAvailable) { return "Прежний K1 можно локально исключить без связи с устройством: действие не отправляет START, STOP или настройки сети. После этого можно отдельно выбрать другой K1."; } return "Безопасная сверка прежнего K1 сейчас недоступна. Обновите состояние; новые команды устройству заблокированы."; } function connectionPhaseFallbackLabel(phase: string | null | undefined): string { if (phase === "device_selected") return "Выбор выполнен"; if (phase === "connected") return "Сетевой адрес получен"; return phaseLabel(phase); } /** * Keep the disconnected connection job focused on its progressive pipeline. * Persisted topology is evidence, not live control authority. Operational * panels return only when they are actionable or required to finish an * already-started lifecycle, especially STOP and recovery. */ export function shouldRenderK1OperationalPanels( state: XgridsK1State | null | undefined, pendingAction: PendingAction | null = null, ): boolean { return Boolean( pendingAction === "live" || hasControlAuthority(state) || state?.source_mode === "live" || state?.source_mode === "replay" || recoverableAcquisition(state) || state?.acquisition?.cleanup_pending === true || requiresCanonicalStopAfterTerminalLocalFailure(state) || isRecoveredPhysicalScanning(state) || isPhysicalStopRecoverySettling(state) || activeStreamRecoveryPresentation(state) !== null ); } export function K1ConnectionPipelines({ controller, desiredConnectionMode, onDesiredConnectionModeChange, operationalPanelsVisible, openSpatialScene, activateAutomaticSpatialSource, sourceLabel, }: { controller: XgridsK1Controller; desiredConnectionMode: ConnectionMode; onDesiredConnectionModeChange: (mode: ConnectionMode) => void | Promise; operationalPanelsVisible: boolean; openSpatialScene: () => void; activateAutomaticSpatialSource: () => void; sourceLabel: string; }) { return ( <> {operationalPanelsVisible ? : null}
{operationalPanelsVisible ? (
) : null}
); } export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) { const controller = useXgridsK1Controller(); const { state, error, errorDiagnostic, errorCorrelation, refresh, clearError, } = controller; const [desiredConnectionMode, setDesiredConnectionMode] = useState( DEFAULT_CONNECTION_MODE, ); const desiredModeInitialized = useRef(false); const desiredModeLocallyDirty = useRef(false); const hydratedScenarioResetKey = useRef(null); const confirmedLive = isConfirmedLiveState(state); const sourceRuntimeBusy = isSourceRuntimeBusy(state); const livePreparationPending = controller.pendingAction === "live"; const preparedAcquisition = recoverableAcquisition(state)?.state === "prepared"; const activeRecoveryPresentation = activeStreamRecoveryPresentation(state); const sourceLabel = activeRecoveryPresentation?.title ?? sourceStatusLabel(state); const releasedAcquisitionFailure = isReleasedTerminalAcquisitionFailure(state); const physicalRecoveryRequired = requiresReadOnlyPhysicalRecovery(state); const savedBridgeNetworkSetupRequired = savedBridgeRequiresNetworkSetup(state); const physicalStopRecoverySettling = isPhysicalStopRecoverySettling(state); const recoveredPhysicalScanning = physicalRecoveryRequired && state?.application_control_session?.state === "scanning" && state.application_control_session.can_stop === true; const physicalRecoveryDetail = physicalRecoveryConnectionDetail(state); const correlatedConnectionAttempt = connectionAttemptForRuntimeError( errorCorrelation, state, ); const showGenericRuntimeError = shouldRenderK1GenericRuntimeError( error, Boolean(correlatedConnectionAttempt), state, errorCorrelation?.action, ); const relevantAcquisitionFailed = state?.source_mode !== "replay" && state?.acquisition?.state === "failed" && !releasedAcquisitionFailure; const projectedPhase = releasedAcquisitionFailure && state?.phase === "error" ? "idle" : state?.phase; const connectionTopology = backendConnectionTopology(state); const effectiveDesiredConnectionMode = desiredModeInitialized.current ? desiredConnectionMode : state?.desired_connection_mode ?? (connectionTopology?.status === "active" ? connectionTopology.connectionMode : DEFAULT_CONNECTION_MODE); useEffect(() => { if (!state || desiredModeInitialized.current) return; desiredModeInitialized.current = true; setDesiredConnectionMode( state.desired_connection_mode ?? (connectionTopology?.status === "active" ? connectionTopology.connectionMode : DEFAULT_CONNECTION_MODE), ); }, [connectionTopology, state]); useEffect(() => { if (!desiredModeInitialized.current) return; const backendDesiredMode = state?.desired_connection_mode; if (!backendDesiredMode) return; const scenarioReset = state?.connection_scenario_reset; const scenarioResetKey = scenarioReset && scenarioReset.revision === state?.desired_connection_mode_revision && scenarioReset.desired_mode === backendDesiredMode && state?.snapshot_runtime_id?.trim() ? `${state.snapshot_runtime_id}:${scenarioReset.revision}` : null; if (scenarioResetKey && hydratedScenarioResetKey.current !== scenarioResetKey) { // The shell emergency reset is an authoritative new backend revision. // It must retire a locally dirty selector too; an older dirty browser // draft cannot keep showing Quick/Direct after canonical Bridge won. hydratedScenarioResetKey.current = scenarioResetKey; desiredModeLocallyDirty.current = false; setDesiredConnectionMode(backendDesiredMode); return; } if (backendDesiredMode === desiredConnectionMode) { desiredModeLocallyDirty.current = false; return; } // Every dropdown gesture is now an explicit backend scenario-reset CAS. // The callback may publish its accepted mode one render before the hook's // authoritative snapshot arrives, so passive polling must not overwrite // that in-flight acknowledgement. Once the backend echoes the exact mode // above, the dirty fence clears and later authoritative changes hydrate it. if (desiredModeLocallyDirty.current) return; setDesiredConnectionMode(backendDesiredMode); }, [ desiredConnectionMode, state?.connection_scenario_reset, state?.desired_connection_mode, state?.desired_connection_mode_revision, state?.snapshot_runtime_id, ]); const updateDesiredConnectionMode = (mode: ConnectionMode) => { desiredModeLocallyDirty.current = mode !== state?.desired_connection_mode; setDesiredConnectionMode(mode); }; const sourceTone: StatusTone = activeRecoveryPresentation ? activeRecoveryPresentation.tone : projectedPhase === "error" || relevantAcquisitionFailed ? "danger" : confirmedLive || state?.source_mode === "replay" ? "success" : sourceRuntimeBusy || preparedAcquisition ? "warning" : "neutral"; const connectionAttemptFailed = !sourceRuntimeBusy && !preparedAcquisition && connectionTopology?.status !== "active" && state?.connection_attempt?.connection_mode === effectiveDesiredConnectionMode && ["failed", "cancelled", "timed_out", "interrupted", "operator_action_required"] .includes(state.connection_attempt.status); const connectionControlBootstrapSettling = isConnectionControlBootstrapSettling(state); const connectionPhaseLabel = livePreparationPending ? "Подготовка приёма" : sourceRuntimeBusy || preparedAcquisition ? sourceLabel : activeRecoveryPresentation ? activeRecoveryPresentation.title : connectionControlBootstrapSettling ? "Подтверждение подключения" : physicalStopRecoverySettling ? "Завершение остановки" : recoveredPhysicalScanning ? "Сканирование продолжается" : physicalRecoveryRequired ? savedBridgeNetworkSetupRequired ? "Нужна настройка сети" : "Требуется действие" : connectionAttemptFailed ? "Подключение не завершено" : projectedPhase === "error" ? connectionPhaseFallbackLabel(projectedPhase) : connectionTopology?.status === "active" ? "Подключение установлено" : connectionTopology?.status === "configured-unverified" ? "Подключение отсутствует" : connectionTopology?.source === "durable" ? "Подключение отсутствует" : connectionTopology?.source === "applied" ? "Подключение отсутствует" : connectionTopology?.source === "last-known" ? "Подключение отсутствует" : connectionPhaseFallbackLabel(projectedPhase); const connectionPhaseTone = livePreparationPending ? "warning" : sourceRuntimeBusy || preparedAcquisition ? sourceTone : activeRecoveryPresentation ? activeRecoveryPresentation.tone : connectionControlBootstrapSettling ? "accent" : physicalRecoveryRequired ? "warning" : connectionAttemptFailed ? "warning" : projectedPhase === "error" ? phaseTone(projectedPhase) : connectionTopology?.status === "active" ? "success" : connectionTopology?.status === "configured-unverified" ? "neutral" : "neutral"; const connectionPhaseDetail = livePreparationPending ? "Подготовка продолжается." : physicalStopRecoverySettling ? "Команда остановки уже принята. Завершение выполняется без повторной команды." : activeRecoveryPresentation ? activeRecoveryPresentation.detail : connectionControlBootstrapSettling ? "Сеть применена. Сервис проверяет управляющее подключение без повторения BLE-команды." : recoveredPhysicalScanning ? "Локальная запись остановлена, но сканирование ещё продолжается." : physicalRecoveryRequired ? physicalRecoveryDetail ?? "Безопасное восстановление прежнего K1 сейчас недоступно." : connectionAttemptFailed ? "Проверьте состояние K1 или начните новое подключение." : !sourceRuntimeBusy && connectionTopology?.status === "configured-unverified" ? "Начните новое подключение." : !sourceRuntimeBusy && connectionTopology?.status === "active" ? "Готово к новой сессии." : "Ожидается состояние локального контура."; const operationalPanelsVisible = shouldRenderK1OperationalPanels( state, controller.pendingAction, ); return (
{showGenericRuntimeError && error ? ( void refresh()} onClear={clearError} /> ) : null}
ЛОКАЛЬНОЕ ПОДКЛЮЧЕНИЕ

Подключение {model.displayName}

Выберите способ связи и последовательно установите подключение.

{connectionPhaseLabel} {connectionPhaseDetail}
); }