fix(k1): restore canonical local connection lifecycle
This commit is contained in:
@@ -17,7 +17,12 @@ import {
|
||||
connectionModeOptions,
|
||||
type ConnectionMode,
|
||||
} from "../configuration";
|
||||
import { provisioningIntentKey } from "../lifecycle";
|
||||
import {
|
||||
canSubmitProvisioningMutation,
|
||||
isReachableConnectionLease,
|
||||
provisioningCandidateById,
|
||||
provisioningIntentKey,
|
||||
} from "../lifecycle";
|
||||
import { finiteMetric } from "../presentation";
|
||||
import type { XgridsK1Controller } from "../runtimeContext";
|
||||
|
||||
@@ -131,39 +136,34 @@ export function K1ProvisioningPipeline({
|
||||
const isBusy = pendingAction !== null;
|
||||
const credentialsReady = connectionMode === "quick-connect"
|
||||
|| (ssid.trim().length > 0 && password.length > 0);
|
||||
const canConnect = powerConfirmed && selectedDeviceId.length > 0 && credentialsReady && !isBusy;
|
||||
const modeCopy = connectionCopy[connectionMode];
|
||||
const selectedModeConnected = Boolean(
|
||||
state?.k1_ip && state.connection_mode === connectionMode,
|
||||
const networkWriteReconciliationPending = Boolean(
|
||||
state?.network_write_reconciliation,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.selected_device_id) {
|
||||
if (state.selected_device_id !== selectedDeviceId) {
|
||||
provisioningIntentRef.current = null;
|
||||
}
|
||||
setSelectedDeviceId(state.selected_device_id);
|
||||
return;
|
||||
}
|
||||
if (selectedDeviceId && state?.devices && !state.devices.some((device) => device.device_id === selectedDeviceId)) {
|
||||
setSelectedDeviceId("");
|
||||
}
|
||||
}, [selectedDeviceId, state?.devices, state?.selected_device_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.connection_mode) {
|
||||
setConnectionMode(state.connection_mode);
|
||||
}
|
||||
}, [state?.connection_mode]);
|
||||
|
||||
const deviceSummary = useMemo(
|
||||
() => devices.find((device) => device.device_id === selectedDeviceId),
|
||||
() => provisioningCandidateById(devices, selectedDeviceId),
|
||||
[devices, selectedDeviceId],
|
||||
);
|
||||
const canConnect = !networkWriteReconciliationPending && canSubmitProvisioningMutation({
|
||||
devices,
|
||||
selectedDeviceId,
|
||||
powerConfirmed,
|
||||
credentialsReady,
|
||||
isBusy,
|
||||
});
|
||||
const modeCopy = connectionCopy[connectionMode];
|
||||
const selectedModeConnected = isReachableConnectionLease(state, connectionMode);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDeviceId && !deviceSummary) {
|
||||
provisioningIntentRef.current = null;
|
||||
setSelectedDeviceId("");
|
||||
}
|
||||
}, [deviceSummary, selectedDeviceId]);
|
||||
|
||||
const canAdoptExistingBridge = connectionMode === "bridge"
|
||||
&& powerConfirmed
|
||||
&& selectedDeviceId.length > 0
|
||||
&& deviceSummary !== undefined
|
||||
&& deviceSummary !== null
|
||||
&& deviceSummary.connectable !== false
|
||||
&& !isBusy;
|
||||
|
||||
const resetProvisioningIntent = () => {
|
||||
@@ -171,14 +171,14 @@ export function K1ProvisioningPipeline({
|
||||
};
|
||||
|
||||
const submitConnect = async () => {
|
||||
if (!canConnect) return;
|
||||
if (!canConnect || !deviceSummary) return;
|
||||
const idempotencyKey = provisioningIntentKey(provisioningIntentRef.current);
|
||||
provisioningIntentRef.current = idempotencyKey;
|
||||
const networkCredentials = connectionMode === "quick-connect"
|
||||
? {}
|
||||
: { ssid: ssid.trim(), password };
|
||||
const succeeded = await connect({
|
||||
device_id: selectedDeviceId,
|
||||
device_id: deviceSummary.device_id,
|
||||
...networkCredentials,
|
||||
connection_mode: connectionMode,
|
||||
compatibility_attestation: profileSelectionForConnectionMode(connectionMode),
|
||||
@@ -187,20 +187,24 @@ export function K1ProvisioningPipeline({
|
||||
if (succeeded) {
|
||||
provisioningIntentRef.current = null;
|
||||
setPassword("");
|
||||
} else if (connectionMode === "quick-connect") {
|
||||
// The backend has already persisted and reconciled the failed bounded
|
||||
// attempt. A later click is a new explicit Quick Connect intent, not an
|
||||
// automatic replay of the consumed operation key.
|
||||
} else {
|
||||
// Every later click is a new explicit operator intent, never an
|
||||
// automatic replay of a consumed failed journal entry. If the prior
|
||||
// write outcome is ambiguous, the backend reconciliation fence blocks
|
||||
// this new intent before another device write for both modes.
|
||||
provisioningIntentRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const submitExistingBridgeAdoption = async () => {
|
||||
if (!canAdoptExistingBridge) return;
|
||||
await verifyConnection({
|
||||
device_id: selectedDeviceId,
|
||||
if (!canAdoptExistingBridge || !deviceSummary) return;
|
||||
const succeeded = await verifyConnection({
|
||||
device_id: deviceSummary.device_id,
|
||||
compatibility_attestation: profileSelectionForConnectionMode("bridge"),
|
||||
});
|
||||
if (succeeded) {
|
||||
provisioningIntentRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -241,8 +245,8 @@ export function K1ProvisioningPipeline({
|
||||
<WizardStep
|
||||
number="02"
|
||||
title="Выберите Bluetooth-устройство"
|
||||
status={pendingAction === "scan" ? "Поиск…" : selectedDeviceId ? "Устройство выбрано" : `Найдено: ${devices.length}`}
|
||||
tone={pendingAction === "scan" ? "accent" : selectedDeviceId ? "success" : "neutral"}
|
||||
status={pendingAction === "scan" ? "Поиск…" : deviceSummary ? "Устройство выбрано" : `Найдено: ${devices.length}`}
|
||||
tone={pendingAction === "scan" ? "accent" : deviceSummary ? "success" : "neutral"}
|
||||
>
|
||||
<p className="step-copy">Поиск занимает 6 секунд и показывает все видимые BLE-устройства. Метка кандидата основана только на имени; точные модель, platform type и прошивка будут проверены по живому DeviceInfo перед START.</p>
|
||||
<Button
|
||||
@@ -282,7 +286,12 @@ export function K1ProvisioningPipeline({
|
||||
<TextField label="Пароль Wi‑Fi" hint="Только в оперативной памяти" type="password" value={password} onChange={(event) => { setPassword(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" placeholder="Введите пароль" />
|
||||
</div>
|
||||
)}
|
||||
<div className="connection-summary"><span>Устройство</span><strong>{deviceSummary?.name || selectedDeviceId || "Сначала выберите устройство"}</strong></div>
|
||||
<div className="connection-summary"><span>Устройство</span><strong>{deviceSummary?.name || deviceSummary?.device_id || "Сначала выберите устройство"}</strong></div>
|
||||
{networkWriteReconciliationPending ? (
|
||||
<p className="safety-note">
|
||||
Предыдущая BLE-запись завершилась до подтверждения актуального состояния K1. Новая запись заблокирована: выберите Bridge и выполните read-only подхват существующего подключения.
|
||||
</p>
|
||||
) : null}
|
||||
<Button width="full" variant="primary" icon={<Icon name="network" />} disabled={!canConnect} onClick={() => void submitConnect()}>
|
||||
{pendingAction === "connect"
|
||||
? connectionMode === "quick-connect" ? "Включаем точку и подключаем…" : "Подключаем…"
|
||||
|
||||
Reference in New Issue
Block a user