Files
NODEDC_MISSION_CORE/plugins/xgrids-k1/frontend/src/physicalCommandConfirmation.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

431 lines
16 KiB
TypeScript

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<Omit<K1PhysicalCommandTarget, "fence">>;
readonly fence: Readonly<K1PhysicalCommandFence>;
readonly fenceKey: string;
}
export interface K1PhysicalCommandConfirmationPayload {
readonly physicalAcceptance: Readonly<OperatorPresenceConfirmation>;
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<string, unknown> | 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<K1PhysicalCommandTarget, "fence">,
): 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, "fence">,
): 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),
});
}