import type { OperatorPresenceConfirmation, XgridsAcquisition, XgridsApplicationControlSession, XgridsConnectionMode, XgridsK1State, } from "./api"; import { currentAppliedConnectionTopology, isSoftwareCommandedAcquisition } from "./lifecycle"; export interface PhysicalConfirmationChecks { operatorPresent: boolean; ownerControlledDevice: boolean; lixelgoClosed: boolean; batteryStorageConfirmed: boolean; expectedPhysicalStateConfirmed: boolean; } type CompletedPhysicalConfirmationChecks = { [Key in keyof PhysicalConfirmationChecks]: true; }; export type K1PhysicalConfirmationKind = "prepare" | "start" | "stop"; /** * Semantic state that authorises one physical command confirmation. * * Timestamps, the polling snapshot revision and ConnectionSupervisor.revision * are deliberately absent: the latter is an observation counter and advances * even when a probe confirms the same semantic route. A read-only refresh must * not invalidate an operator confirmation. Every field below, however, changes * the identity, route, CAS authority, acquisition or runtime state of the * command and therefore closes an already-open modal. */ export interface K1PhysicalCommandFence { kind: K1PhysicalConfirmationKind; commandDeviceId: string; commandProjectName: string; acquisitionId: string; runtimeId: string | null; runtimePhase: string | null; runtimeSourceMode: string | null; selectedDeviceId: string | null; deviceRefId: string | null; deviceSessionId: string | null; deviceSessionDeviceId: string | null; deviceSessionConnectivity: string | null; connectionIntentId: string | null; requestedConnectionMode: XgridsConnectionMode | null; expectedDeviceId: string | null; deviceNetworkState: string | null; deviceNetworkIntentId: string | null; transportRef: string | null; connectionMode: XgridsConnectionMode | null; targetIpv4: string | null; targetPort: number | null; hostPathEpoch: number | null; hostPathAvailable: boolean | null; deviceIdentityState: string | null; deviceIdentityId: string | null; controlPlaneState: string | null; controlPlaneSessionId: string | null; dataPlaneState: string | null; dataPlaneSessionId: string | null; leaseState: string | null; leaseGeneration: number | null; controlAllowed: boolean | null; acquisitionStartAllowed: boolean | null; dataIngestAuthoritative: boolean | null; controlSessionGeneration: number | null; controlStateRevision: number | null; controlState: string | null; controlSocketOpen: boolean | null; verifiedControlSessionId: string | null; controlProofRevision: number | null; controlProofFresh: boolean | null; deviceReportedState: string | null; deviceProjectBound: boolean | null; deviceInitReady: boolean | null; acquisitionState: string | null; acquisitionStateRevision: number | null; acquisitionDeviceId: string | null; acquisitionDeviceSessionId: string | null; acquisitionControlMode: string | null; } export interface K1PhysicalCommandTarget { deviceId: string; connection: string; projectName: string; acquisitionId: string; deviceState: string; fence: K1PhysicalCommandFence; } export interface K1PhysicalCommandCheckpoint { readonly kind: K1PhysicalConfirmationKind; readonly target: Readonly>; readonly fence: Readonly; readonly fenceKey: string; } export interface K1PhysicalCommandConfirmationPayload { readonly physicalAcceptance: Readonly; readonly checkpoint: K1PhysicalCommandCheckpoint; } export function emptyPhysicalConfirmationChecks(): PhysicalConfirmationChecks { return { operatorPresent: false, ownerControlledDevice: false, lixelgoClosed: false, batteryStorageConfirmed: false, expectedPhysicalStateConfirmed: false, }; } export function physicalConfirmationComplete( checks: PhysicalConfirmationChecks, ): checks is CompletedPhysicalConfirmationChecks { return ( checks.operatorPresent && checks.ownerControlledDevice && checks.lixelgoClosed && checks.batteryStorageConfirmed && checks.expectedPhysicalStateConfirmed ); } export function operatorPresenceConfirmation( checks: PhysicalConfirmationChecks, ): OperatorPresenceConfirmation | null { if (!physicalConfirmationComplete(checks)) return null; return { operator_present: checks.operatorPresent, owner_controlled_device: checks.ownerControlledDevice, lixelgo_closed: checks.lixelgoClosed, battery_storage_confirmed: checks.batteryStorageConfirmed, expected_physical_state_confirmed: checks.expectedPhysicalStateConfirmed, }; } /** * One deliberate click on the local K1 START/STOP action is the operator's * physical acceptance. The backend still validates the exact control CAS, * live DeviceInfo/status binding and command ledger before a vendor write; * this helper only removes the redundant five-checkbox modal. */ export function operatorActionPhysicalAcceptance(): OperatorPresenceConfirmation { return { operator_present: true, owner_controlled_device: true, lixelgo_closed: true, battery_storage_confirmed: true, expected_physical_state_confirmed: true, }; } function recordValue( record: Record | null | undefined, key: string, ): unknown { return record?.[key]; } function trimmed(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim() : null; } function integer(value: unknown): number | null { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null; } function boolean(value: unknown): boolean | null { return typeof value === "boolean" ? value : null; } function commandFence( kind: K1PhysicalConfirmationKind, state: XgridsK1State | null | undefined, target: Omit, ): K1PhysicalCommandFence { const supervisor = state?.connection_supervisor; const deviceNetwork = supervisor?.observed.device_network; const hostPath = supervisor?.observed.host_path; const deviceIdentity = supervisor?.observed.device_identity; const controlPlane = supervisor?.observed.control_plane; const dataPlane = supervisor?.observed.data_plane; const control = state?.application_control_session; const verifiedControl = control?.verified_control; const acquisition = state?.acquisition; return { kind, commandDeviceId: target.deviceId, commandProjectName: target.projectName, acquisitionId: target.acquisitionId, runtimeId: trimmed(state?.snapshot_runtime_id), runtimePhase: trimmed(state?.phase), runtimeSourceMode: trimmed(state?.source_mode), selectedDeviceId: trimmed(state?.selected_device_id), deviceRefId: trimmed(state?.device_ref?.device_id), deviceSessionId: trimmed(state?.device_session?.device_session_id), deviceSessionDeviceId: trimmed(state?.device_session?.device_id), deviceSessionConnectivity: trimmed(state?.device_session?.connectivity), connectionIntentId: trimmed(supervisor?.intent?.intent_id), requestedConnectionMode: supervisor?.intent?.requested_mode ?? null, expectedDeviceId: trimmed(supervisor?.intent?.expected_device_id), deviceNetworkState: trimmed(deviceNetwork?.state), deviceNetworkIntentId: trimmed(deviceNetwork?.intent_id), transportRef: trimmed(deviceNetwork?.transport_ref), connectionMode: deviceNetwork?.connection_mode ?? null, targetIpv4: trimmed(deviceNetwork?.target?.ipv4), targetPort: integer(deviceNetwork?.target?.port), hostPathEpoch: integer(hostPath?.epoch), hostPathAvailable: boolean(hostPath?.available), deviceIdentityState: trimmed(deviceIdentity?.state), deviceIdentityId: trimmed(deviceIdentity?.logical_device_id), controlPlaneState: trimmed(controlPlane?.state), controlPlaneSessionId: trimmed(controlPlane?.session_id), dataPlaneState: trimmed(dataPlane?.state), dataPlaneSessionId: trimmed(dataPlane?.session_id), leaseState: trimmed(supervisor?.lease.state), leaseGeneration: integer(supervisor?.lease.generation), controlAllowed: boolean(supervisor?.authority.control_allowed), acquisitionStartAllowed: boolean(supervisor?.authority.acquisition_start_allowed), dataIngestAuthoritative: boolean(supervisor?.authority.data_ingest_authoritative), controlSessionGeneration: integer(control?.session_generation), controlStateRevision: integer(control?.state_revision), controlState: trimmed(control?.state), controlSocketOpen: boolean(control?.control_socket_open), verifiedControlSessionId: trimmed(verifiedControl?.control_session_id), controlProofRevision: integer(verifiedControl?.control_proof_revision), controlProofFresh: boolean(verifiedControl?.control_proof_fresh), deviceReportedState: trimmed(recordValue(control?.transport, "latest_device_session_state")), deviceProjectBound: boolean(recordValue(control?.transport, "latest_device_project_bound")), deviceInitReady: boolean(recordValue(control?.transport, "latest_device_init_ready")), acquisitionState: trimmed(acquisition?.state), acquisitionStateRevision: integer(acquisition?.state_revision), acquisitionDeviceId: trimmed(acquisition?.device_id), acquisitionDeviceSessionId: trimmed(acquisition?.device_session_id), acquisitionControlMode: trimmed(acquisition?.control_mode), }; } export function physicalCommandFenceKey( kind: K1PhysicalConfirmationKind, target: K1PhysicalCommandTarget, ): string { // Both values are included. This makes a mismatched component kind fail // closed even if a caller accidentally supplies a target built for another // physical command. return JSON.stringify([ kind, target.deviceId, target.connection, target.projectName, target.acquisitionId, target.deviceState, target.fence, ]); } export function createPhysicalCommandCheckpoint( kind: K1PhysicalConfirmationKind, target: K1PhysicalCommandTarget, ): K1PhysicalCommandCheckpoint { const fence = Object.freeze({ ...target.fence }); const targetSnapshot = Object.freeze({ deviceId: target.deviceId, connection: target.connection, projectName: target.projectName, acquisitionId: target.acquisitionId, deviceState: target.deviceState, }); return Object.freeze({ kind, target: targetSnapshot, fence, fenceKey: physicalCommandFenceKey(kind, target), }); } export function physicalCommandCheckpointMatches( checkpoint: K1PhysicalCommandCheckpoint, kind: K1PhysicalConfirmationKind, target: K1PhysicalCommandTarget, ): boolean { return checkpoint.kind === kind && checkpoint.fence.kind === kind && target.fence.kind === kind && checkpoint.fenceKey === physicalCommandFenceKey(kind, target); } function targetWithFence( kind: K1PhysicalConfirmationKind, state: XgridsK1State | null | undefined, target: Omit, ): K1PhysicalCommandTarget { return { ...target, fence: commandFence(kind, state, target), }; } function exactReadyState( control: XgridsApplicationControlSession, ): string | null { const deviceState = trimmed(recordValue(control.transport, "latest_device_session_state")); const projectBound = recordValue(control.transport, "latest_device_project_bound"); const initReady = recordValue(control.transport, "latest_device_init_ready"); if (deviceState !== "ready" || projectBound !== true || initReady !== false) return null; return "READY · проект привязан · инициализация не запущена"; } function exactScanningState( control: XgridsApplicationControlSession | null | undefined, ): string { const deviceState = trimmed(recordValue(control?.transport, "latest_device_session_state")); const projectBound = recordValue(control?.transport, "latest_device_project_bound"); const initReady = recordValue(control?.transport, "latest_device_init_ready"); if (deviceState === "scanning" && projectBound === true && initReady === true) { return "SCANNING · проект привязан · инициализация завершена"; } return deviceState ? `${deviceState.toUpperCase()} · последнее подтверждённое состояние K1` : "Состояние K1 не подтверждено текущим управляющим каналом"; } function exactConnection( control: XgridsApplicationControlSession | null | undefined, ): string | null { const verified = control?.verified_control; if (!verified) return null; return `${verified.connection_mode} · ${verified.target_ipv4}:${verified.target_port}`; } function acquisitionProject(acquisition: XgridsAcquisition): string { return trimmed(acquisition.project_name) ?? "Проект без опубликованного имени"; } export function preparedStartTarget( state: XgridsK1State | null | undefined, ): K1PhysicalCommandTarget | null { const acquisition = state?.acquisition; const control = state?.application_control_session; const verified = control?.verified_control; const topology = currentAppliedConnectionTopology(state); const supervisor = state?.connection_supervisor; const deviceNetwork = supervisor?.observed.device_network; const hostPath = supervisor?.observed.host_path; const readyState = control ? exactReadyState(control) : null; if ( !acquisition || acquisition.state !== "prepared" || acquisition.control_mode !== "plugin-commanded" || !control || control.state !== "project-ready" || control.can_start !== true || !verified || verified.control_proof_fresh !== true || verified.logical_device_id !== acquisition.device_id || verified.compatibility_profile_id !== acquisition.compatibility_profile_id || supervisor?.authority.acquisition_start_allowed !== true || verified.intent_id !== supervisor.intent?.intent_id || verified.host_path_epoch !== hostPath?.epoch || verified.transport_ref !== deviceNetwork?.transport_ref || verified.connection_mode !== deviceNetwork?.connection_mode || verified.target_ipv4 !== deviceNetwork?.target?.ipv4 || verified.target_port !== deviceNetwork?.target?.port || topology?.status !== "active" || topology.connectionMode !== verified.connection_mode || topology.endpoint !== verified.target_ipv4 || state?.connection_lifecycle?.ready_to_start !== true || !readyState ) { return null; } return targetWithFence("start", state, { deviceId: verified.logical_device_id, connection: `${verified.connection_mode} · ${verified.target_ipv4}:${verified.target_port}`, projectName: acquisitionProject(acquisition), acquisitionId: acquisition.acquisition_id, deviceState: readyState, }); } export function preparationTarget( state: XgridsK1State | null | undefined, projectName: string, ): K1PhysicalCommandTarget | null { const topology = currentAppliedConnectionTopology(state); const supervisor = state?.connection_supervisor; const deviceNetwork = supervisor?.observed.device_network; if ( !topology || !supervisor || topology.status === "configured-offline" || !deviceNetwork?.transport_ref || !deviceNetwork.target ) { return null; } const logicalDeviceId = supervisor.observed.device_identity.logical_device_id ?? supervisor.intent?.expected_device_id ?? deviceNetwork.transport_ref; return targetWithFence("prepare", state, { deviceId: logicalDeviceId, connection: `${topology.connectionMode} · ${deviceNetwork.target.ipv4}:${deviceNetwork.target.port}`, projectName: projectName.trim(), acquisitionId: "Будет создана подготовительным этапом; START пока недоступен", deviceState: "Подготовка не начата · физический START не разрешён", }); } export function activeStopTarget( state: XgridsK1State | null | undefined, ): K1PhysicalCommandTarget | null { const acquisition = state?.acquisition; if (!acquisition || !isSoftwareCommandedAcquisition(state)) return null; const control = state?.application_control_session; return targetWithFence("stop", state, { deviceId: acquisition.device_id, connection: exactConnection(control) ?? "Текущий управляющий канал не подтверждён", projectName: acquisitionProject(acquisition), acquisitionId: acquisition.acquisition_id, deviceState: exactScanningState(control), }); }