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.
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
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),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user