From d01be34cac557db9f4d9b7a3c1353b28e0f85f77 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Thu, 20 Aug 2026 14:04:55 +0300 Subject: [PATCH] fix(k1): expose exact supervisor recovery verify --- .../test/k1SupervisorPresentation.test.mjs | 84 +++++++++++++++++++ plugins/xgrids-k1/frontend/src/lifecycle.ts | 58 +++++++++++-- 2 files changed, 134 insertions(+), 8 deletions(-) diff --git a/apps/control-station/test/k1SupervisorPresentation.test.mjs b/apps/control-station/test/k1SupervisorPresentation.test.mjs index 959e705..56af9c9 100644 --- a/apps/control-station/test/k1SupervisorPresentation.test.mjs +++ b/apps/control-station/test/k1SupervisorPresentation.test.mjs @@ -5260,6 +5260,90 @@ test("an unresolved physical command has one explicit server-bound read-only rec assert.match(physicalRecovery, /physicalRecoveryTarget\?\.serverBound/); }); +test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible", () => { + const state = durableTopologyState(); + const recoveryTransport = "F89438FA-55ED-85AD-EED7-734AC84746D8"; + state.snapshot_runtime_id = "runtime-terminal-control-physical-recovery"; + state.semantic_topology_store.record.transport_ref = recoveryTransport; + state.physical_command = { + status: "unresolved", + reason_code: "physical-command-reconciliation-required", + requires_reconciliation: true, + resolved_active_recovery_required: false, + reconciliation_ready: true, + record: { + revision: 583, + operation_id: "old-start-operation", + action: "start", + stage: "observing", + resolution: null, + connection: { + transport_ref: recoveryTransport, + connection_mode: "bridge", + target_ipv4: "192.168.68.52", + }, + }, + }; + const deniedObservation = (targetSource) => ({ + allowed: false, + reason_codes: ["control-session-not-admissible-for-network-change"], + target_source: targetSource, + required_transport_ref: recoveryTransport, + required_connection_mode: "bridge", + requires_live_gatt_validation: true, + automatic_retry: false, + }); + state.connection_policy = { + schema_version: "missioncore.xgrids-k1-connection-policy/v1", + facts: { retained_context_is_presence: false, retired_transport_refs: [] }, + recommended_action: "scan-ble", + allowed_actions: [ + "scan-ble", + "inspect-configured-endpoint", + "verify-control-device-info", + ], + actions: { + "observe-fresh-device-network": deniedObservation("fresh-scan"), + "observe-current-device-network": deniedObservation( + "retained-current-process", + ), + "observe-configured-device-network": deniedObservation( + "durable-configured-state", + ), + "verify-control-device-info": { + allowed: true, + reason_codes: [], + target_source: "connection-supervisor", + required_transport_ref: null, + requires_live_gatt_validation: false, + automatic_retry: false, + }, + }, + }; + + assert.deepEqual(readOnlyConnectionObservationTarget(state), { + action: "verify-control-device-info", + deviceId: recoveryTransport, + connectionMode: "bridge", + source: "durable-configured-state", + serverBound: true, + expectedDiscoveryGeneration: null, + }); + + const markup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, { + controller: provisioningController(state), + desiredMode: "bridge", + })); + const reconnect = buttonMarkupWithText(markup, "Переподключиться"); + assert.equal(reconnect.length, 1); + assert.doesNotMatch(reconnect[0], /\bdisabled(?:=|\s|>)/); + assert.equal(buttonMarkupWithText(markup, "Подключить новый K1").length, 1); + + const mismatched = structuredClone(state); + mismatched.semantic_topology_store.record.transport_ref = "another-k1"; + assert.equal(readOnlyConnectionObservationTarget(mismatched), null); +}); + test("unavailable physical replacement stays CAS-protected behind the simple new-device path", () => { const state = durableTopologyState(); const transportRef = "F89438FA-55ED-85AD-EED7-734AC84746D8"; diff --git a/plugins/xgrids-k1/frontend/src/lifecycle.ts b/plugins/xgrids-k1/frontend/src/lifecycle.ts index 4ea0b4c..a188085 100644 --- a/plugins/xgrids-k1/frontend/src/lifecycle.ts +++ b/plugins/xgrids-k1/frontend/src/lifecycle.ts @@ -1096,7 +1096,7 @@ export function readOnlyPhysicalRecoveryBinding( state: XgridsK1State | null | undefined, ): ReadOnlyPhysicalRecoveryBinding | null { if (!requiresReadOnlyPhysicalRecovery(state)) return null; - const actions: ReadOnlyConnectionObservationAction[] = [ + const actions: ReadOnlyNetworkObservationAction[] = [ "observe-fresh-device-network", "observe-current-device-network", "observe-configured-device-network", @@ -1124,13 +1124,17 @@ export function canSelectConnectionMode( ); } -export type ReadOnlyConnectionObservationAction = Extract< +type ReadOnlyNetworkObservationAction = Extract< XgridsConnectionPolicyAction, | "observe-fresh-device-network" | "observe-current-device-network" | "observe-configured-device-network" >; +export type ReadOnlyConnectionObservationAction = + | ReadOnlyNetworkObservationAction + | Extract; + export type ReadOnlyConnectionObservationSource = | "fresh-scan" | "retained-current-process" @@ -1153,7 +1157,7 @@ function isConnectionMode(value: unknown): value is XgridsConnectionMode { function exactPolicyObservationTarget( state: XgridsK1State | null | undefined, - action: ReadOnlyConnectionObservationAction, + action: ReadOnlyNetworkObservationAction, source: ReadOnlyConnectionObservationSource, ): ReadOnlyConnectionObservationTarget | null { if (!connectionPolicyAllows(state, action)) return null; @@ -1189,7 +1193,7 @@ function exactPolicyObservationTarget( } const SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY: -ReadonlyArray = [ +ReadonlyArray = [ "observe-current-device-network", "observe-configured-device-network", "observe-fresh-device-network", @@ -1206,17 +1210,17 @@ export function recommendedConnectionRecoveryObservationTarget( ): ReadOnlyConnectionObservationTarget | null { const recommended = state?.connection_policy?.recommended_action; const orderedActions = SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY.includes( - recommended as ReadOnlyConnectionObservationAction, + recommended as ReadOnlyNetworkObservationAction, ) ? [ - recommended as ReadOnlyConnectionObservationAction, + recommended as ReadOnlyNetworkObservationAction, ...SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY.filter( (action) => action !== recommended, ), ] : SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY; const sources: Record< - ReadOnlyConnectionObservationAction, + ReadOnlyNetworkObservationAction, ReadOnlyConnectionObservationSource > = { "observe-current-device-network": "retained-current-process", @@ -1230,6 +1234,44 @@ export function recommendedConnectionRecoveryObservationTarget( return null; } +/** + * A terminal control session may block the network-observation actions while + * the supervisor still explicitly allows its narrower DeviceInfo Verify. + * Reuse that permission only when the physical ledger and durable topology + * independently pin the same K1 and mode; browser selection is never used. + */ +function exactControlVerificationTarget( + state: XgridsK1State | null | undefined, +): ReadOnlyConnectionObservationTarget | null { + if ( + !requiresReadOnlyPhysicalRecovery(state) + || !connectionPolicyAllows(state, "verify-control-device-info") + ) return null; + + const binding = readOnlyPhysicalRecoveryBinding(state); + const semanticStore = state?.semantic_topology_store; + const durable = semanticStore?.record; + if ( + !binding + || semanticStore?.status !== "available" + || semanticStore.configured_offline_evidence !== true + || semanticStore.live_connection_authority !== false + || durable?.schema_version !== "missioncore.xgrids-k1-semantic-topology/v1" + || transportRefEquivalenceKey(durable.transport_ref) + !== transportRefEquivalenceKey(binding.deviceId) + || durable.connection_mode !== binding.connectionMode + ) return null; + + return { + action: "verify-control-device-info", + deviceId: binding.deviceId, + connectionMode: binding.connectionMode, + source: "durable-configured-state", + serverBound: true, + expectedDiscoveryGeneration: null, + }; +} + /** * Resolve the one read-only BLE target authorized by the server policy. * Unresolved writes never fall back to a browser selection or dropdown mode: @@ -1285,7 +1327,7 @@ export function readOnlyConnectionObservationTarget( state, "observe-configured-device-network", "durable-configured-state", - ); + ) ?? exactControlVerificationTarget(state); } /**