chore(k1): checkpoint connection recovery work
This commit is contained in:
@@ -293,9 +293,6 @@ export function K1AcquisitionPipeline({
|
||||
projectNameValidation.value,
|
||||
);
|
||||
const physicalStartAllowed = connectionPolicyAllows(state, "start-acquisition");
|
||||
const physicalStartGuidance = finalStartTarget && !physicalStartAllowed
|
||||
? connectionPolicyOperatorGuidance(state, "start-acquisition")
|
||||
: null;
|
||||
const physicalStopGuidance = gracefulStopTarget
|
||||
&& !physicalStopPresented
|
||||
&& !terminalPhysicalStopObserved
|
||||
@@ -317,8 +314,9 @@ export function K1AcquisitionPipeline({
|
||||
? "Команда устройству недоступна в текущем подтверждённом состоянии. Завершите только локальный приём или выполните read-only восстановление."
|
||||
: null;
|
||||
|
||||
const submitFinalStart = async () => {
|
||||
const physicalAcceptance = operatorActionPhysicalAcceptance();
|
||||
const submitFinalStart = async (
|
||||
physicalAcceptance = operatorActionPhysicalAcceptance(),
|
||||
) => {
|
||||
await runAutomaticSpatialSourceStart(
|
||||
() => startPreparedAcquisition(physicalAcceptance),
|
||||
activateAutomaticSpatialSource,
|
||||
@@ -342,6 +340,7 @@ export function K1AcquisitionPipeline({
|
||||
return;
|
||||
}
|
||||
if (!draftPreparationTarget) return;
|
||||
const physicalAcceptance = operatorActionPhysicalAcceptance();
|
||||
const prepared = await prepareCanonicalAcquisition({
|
||||
acquisition: {
|
||||
project_name: projectNameValidation.value,
|
||||
@@ -349,9 +348,10 @@ export function K1AcquisitionPipeline({
|
||||
gnss_mode: SUPPORTED_GNSS_MODE,
|
||||
compatibility_attestation: profileSelectionForConnectionMode(connectionMode),
|
||||
},
|
||||
physicalAcceptance,
|
||||
});
|
||||
if (!prepared) return;
|
||||
await submitFinalStart();
|
||||
await submitFinalStart(physicalAcceptance);
|
||||
};
|
||||
|
||||
const submitReplay = async () => {
|
||||
@@ -458,7 +458,6 @@ export function K1AcquisitionPipeline({
|
||||
</div>
|
||||
<TextField
|
||||
label="Название проекта"
|
||||
hint="Имя войдёт в единственный канонический START"
|
||||
value={projectName}
|
||||
onChange={(event) => {
|
||||
setProjectName(event.target.value);
|
||||
@@ -470,7 +469,7 @@ export function K1AcquisitionPipeline({
|
||||
aria-invalid={projectNameTouched && projectNameValidation.error ? true : undefined}
|
||||
description={projectNameTouched && projectNameValidation.error
|
||||
? projectNameValidation.error
|
||||
: "Имя отправляется только при START; отдельной команды сохранения нет."}
|
||||
: undefined}
|
||||
placeholder="Например, TEST001"
|
||||
/>
|
||||
<Button
|
||||
@@ -507,15 +506,6 @@ export function K1AcquisitionPipeline({
|
||||
? "Продолжить запуск"
|
||||
: "Запустить приём"}
|
||||
</Button>
|
||||
<p className="start-confirmation-note">
|
||||
{modeSwitchRequired
|
||||
? `Выбран другой способ связи. Сначала установите подключение через ${desiredConnectionMode === "bridge" ? "Bridge" : desiredConnectionMode === "quick-connect" ? "Quick Connect" : "Direct Connect"}.`
|
||||
: !connectionConfigured
|
||||
? "Сначала завершите подключение в выбранном режиме. START не используется для установки связи."
|
||||
: physicalStartGuidance
|
||||
? `${physicalStartGuidance.reason} ${physicalStartGuidance.nextAction}`
|
||||
: "Одно нажатие выполняет каноническую подготовку и один START после подтверждённого READY. Автоматических повторов команд нет."}
|
||||
</p>
|
||||
{control?.control_socket_open && !activeAcquisition && !recoveredPhysicalStop && !isBusy ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -525,21 +515,6 @@ export function K1AcquisitionPipeline({
|
||||
Отменить запуск до START
|
||||
</Button>
|
||||
) : null}
|
||||
<p className="live-instruction">
|
||||
{controlPhase === "failed"
|
||||
? `Диалог остановлен без автоповтора: ${control?.failure?.message || "требуется ручное действие"}`
|
||||
: controlPhase === "connecting"
|
||||
? "Выполняются операции 1–6 записанного диалога; следующий этап ждёт подтверждённый ответ."
|
||||
: controlPhase === "workspace-requested"
|
||||
? "После подтверждённых операций 1–6 выполняется вход в рабочее пространство."
|
||||
: controlPhase === "project-requested"
|
||||
? "Выполняются операции 8–10 и готовится локальный приём; имя ещё не отправляется."
|
||||
: controlPhase === "start-requested" || controlPhase === "initializing"
|
||||
? "Калибровка оборудования. Не перемещайте сканер; временных переходов и повторных команд нет."
|
||||
: controlPhase === "scanning"
|
||||
? "Режим сканирования и инициализация подтверждены. Остановка доступна в пространственной сцене."
|
||||
: "Одна кнопка запускает весь процесс. Совместимость подключения подтверждается автоматически; каждый следующий этап начинается только после подтверждения результата."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="session-form session-form--replay">
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
requiresReadOnlyPhysicalRecovery,
|
||||
reopenedPhysicalReconciliationMatches,
|
||||
retiredPhysicalReopenAuthority,
|
||||
savedBridgeRequiresNetworkSetup,
|
||||
serverBoundAppliedNetworkObservationTarget,
|
||||
transportRefEquivalenceKey,
|
||||
trustedConnectionBinding,
|
||||
@@ -358,12 +359,11 @@ export function connectionAttemptOwnsAppliedNetworkRecovery(
|
||||
|| attempt.phase !== "network_applied"
|
||||
|| attempt.control_state === "ready"
|
||||
) return false;
|
||||
if (["accepted", "running"].includes(attempt.status)) return true;
|
||||
return [
|
||||
"continue-with-control-verification",
|
||||
"verify-control-read-only",
|
||||
"manual-recovery-required",
|
||||
].includes(attempt.safe_next_action);
|
||||
// Only an operation that is still executing owns the screen. A terminal
|
||||
// network-applied record is audit history: the backend may continue to use
|
||||
// it as a command fence, but it must never replace the operator's two normal
|
||||
// choices (verify the saved K1 or start a fresh Bluetooth connection).
|
||||
return ["accepted", "running"].includes(attempt.status);
|
||||
}
|
||||
|
||||
export async function dispatchUnavailablePhysicalRetirementForCurrentRuntime<T>(
|
||||
@@ -1537,6 +1537,7 @@ export function K1ProvisioningPipeline({
|
||||
typeof isSnapshotRuntimeCurrent !== "function"
|
||||
|| runtimeActionIsCurrent(currentRuntimeActionFence)
|
||||
) ? pendingAction : null;
|
||||
const livePreparationPending = pendingAction === "live";
|
||||
const reconfiguration = activeConnectionReconfiguration(state);
|
||||
const reconfigurationRevision = state?.connection_reconfiguration?.revision ?? 0;
|
||||
const reconfigurationIntentId = reconfiguration?.intent_id ?? null;
|
||||
@@ -1660,7 +1661,8 @@ export function K1ProvisioningPipeline({
|
||||
const physicalRecoveryVerificationPending =
|
||||
currentReadOnlyReconnectPresentation?.kind === "physical";
|
||||
const provisioningMutationBusy = Boolean(
|
||||
presentedPendingAction
|
||||
livePreparationPending
|
||||
|| presentedPendingAction
|
||||
|| modeResetPending !== null
|
||||
|| currentPreparingReconfigurationRequest
|
||||
|| searchDisplayActive
|
||||
@@ -1731,6 +1733,11 @@ export function K1ProvisioningPipeline({
|
||||
const appliedNetworkAttempt = state?.connection_attempt?.phase === "network_applied"
|
||||
? state.connection_attempt
|
||||
: null;
|
||||
const terminalAppliedRecoveryAttempt = appliedNetworkAttempt
|
||||
&& appliedNetworkAttempt.control_state !== "ready"
|
||||
&& !["accepted", "running"].includes(appliedNetworkAttempt.status)
|
||||
? appliedNetworkAttempt
|
||||
: null;
|
||||
const correlatedConnectionAttempt = connectionAttemptForRuntimeError(
|
||||
errorCorrelation,
|
||||
state,
|
||||
@@ -1741,9 +1748,11 @@ export function K1ProvisioningPipeline({
|
||||
? state.connection_attempt
|
||||
: null;
|
||||
const connectionRecoveryAttempt = correlatedConnectionAttempt
|
||||
?? unknownNetworkOutcomeAttempt;
|
||||
?? unknownNetworkOutcomeAttempt
|
||||
?? terminalAppliedRecoveryAttempt;
|
||||
const connectionRecoveryObservationAllowed = Boolean(
|
||||
!connectionRecoveryAttempt
|
||||
|| connectionRecoveryAttempt.phase === "network_applied"
|
||||
|| [
|
||||
"continue-with-control-verification",
|
||||
"verify-control-read-only",
|
||||
@@ -1780,15 +1789,14 @@ export function K1ProvisioningPipeline({
|
||||
const physicalRecoveryRequired = requiresReadOnlyPhysicalRecovery(state);
|
||||
const networkRecoveryRequired = unresolvedAppliedAttempt !== null
|
||||
&& !physicalRecoveryRequired;
|
||||
// Scan keeps the reset-owned clean draft intact, while the first later
|
||||
// Connect/Verify operation releases it. This hides only pre-reset saved
|
||||
// history and never masks a new post-reset failure.
|
||||
// A settled reset owns the clean new-device draft, but it must not erase the
|
||||
// separately durable fast-reconnect offer. The operator can either verify
|
||||
// the last confirmed K1 without writes or start a fresh Bluetooth search.
|
||||
const scenarioResetOwnsCleanDraft = scenarioResetOwnsCleanConnectionDraft(
|
||||
state,
|
||||
connectionMode,
|
||||
);
|
||||
const connectionRecoveryTarget = !scenarioResetOwnsCleanDraft
|
||||
&& !physicalRecoveryRequired
|
||||
const connectionRecoveryTarget = !physicalRecoveryRequired
|
||||
&& !unresolvedAppliedAttempt
|
||||
&& !selectedModeConnected
|
||||
&& !reconfigurationActive
|
||||
@@ -1805,10 +1813,12 @@ export function K1ProvisioningPipeline({
|
||||
connectionRecoveryKey,
|
||||
escapedConnectionRecoveryKey,
|
||||
) && !(searchRequested && !searchDisplayActive);
|
||||
const presentedConnectionRecoveryRequired = connectionRecoveryRequired
|
||||
|| connectionRecoveryVerificationPending;
|
||||
const presentedConnectionRecoveryRequired = !livePreparationPending && (
|
||||
connectionRecoveryRequired || connectionRecoveryVerificationPending
|
||||
);
|
||||
const presentedPhysicalRecoveryRequired = physicalRecoveryRequired
|
||||
|| physicalRecoveryVerificationPending;
|
||||
const savedBridgeNetworkSetupRequired = savedBridgeRequiresNetworkSetup(state);
|
||||
const appliedControlSettlementPending = Boolean(
|
||||
unresolvedAppliedAttempt
|
||||
&& ["accepted", "running"].includes(unresolvedAppliedAttempt.status)
|
||||
@@ -1893,10 +1903,12 @@ export function K1ProvisioningPipeline({
|
||||
&& !physicalStopRecoverySettling,
|
||||
);
|
||||
const trustedBinding = trustedConnectionBinding(state);
|
||||
const connectedEndpoint = selectedModeConnected
|
||||
const connectionPresentationEstablished = selectedModeConnected
|
||||
|| livePreparationPending;
|
||||
const connectedEndpoint = connectionPresentationEstablished
|
||||
? selectedModeTopology?.endpoint?.trim() || null
|
||||
: null;
|
||||
const connectedDeviceIdentity = selectedModeConnected
|
||||
const connectedDeviceIdentity = connectionPresentationEstablished
|
||||
? selectedTarget?.label
|
||||
|| retainedSessionLabel
|
||||
|| (trustedBinding?.connectionMode === connectionMode
|
||||
@@ -1951,7 +1963,7 @@ export function K1ProvisioningPipeline({
|
||||
!presentedPhysicalRecoveryRequired
|
||||
&& !presentedConnectionRecoveryRequired
|
||||
&& (
|
||||
selectedModeConnected
|
||||
connectionPresentationEstablished
|
||||
|| explicitProvisioningDraftRetained
|
||||
|| explicitProvisioningDraftContextRetained
|
||||
|| unresolvedAppliedAttempt
|
||||
@@ -2619,10 +2631,13 @@ export function K1ProvisioningPipeline({
|
||||
) return;
|
||||
if (!dispatched.result.succeeded) {
|
||||
setCandidateUnavailableMessage(
|
||||
"Не удалось переподключиться к прежнему K1. Проверьте питание и сеть, затем повторите проверку.",
|
||||
dispatched.result.reasonCode === "connection-verify-address-unavailable"
|
||||
? "K1 ответил по Bluetooth, но не подключён к сохранённой общей сети. Старый Bridge использовать нельзя; подключите K1 к общей сети заново — настройки устройства не менялись."
|
||||
: "Не удалось переподключиться к прежнему K1. Проверьте питание и сеть, затем повторите проверку.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
clearError();
|
||||
setCandidateUnavailableMessage(null);
|
||||
} finally {
|
||||
setReadOnlyReconnectPresentation((current) => (
|
||||
@@ -2699,14 +2714,20 @@ export function K1ProvisioningPipeline({
|
||||
const actionFence = activateRuntimeActionFence(modeAuthority);
|
||||
if (!actionFence || !runtimeActionIsCurrent(actionFence)) return;
|
||||
try {
|
||||
const result = await verifyConnection(request);
|
||||
const result = await verifyConnection(request, { surfaceErrors: false });
|
||||
if (!runtimeClickIsCurrent(actionFence)) return;
|
||||
if (!result.succeeded) {
|
||||
setCandidateUnavailableMessage(
|
||||
"Не удалось переподключиться к прежнему K1. Проверьте питание и сеть, затем повторите проверку.",
|
||||
result.reasonCode === "connection-verify-address-unavailable"
|
||||
? "K1 ответил по Bluetooth, но не подключён к сохранённой общей сети. Старый Bridge использовать нельзя; подключите K1 к общей сети заново — настройки устройства не менялись."
|
||||
: "Не удалось переподключиться к прежнему K1. Проверьте питание и сеть, затем повторите проверку.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// A successful authoritative Verify supersedes any earlier local
|
||||
// connection banner, including a failure that settled immediately
|
||||
// before the recovered state reached this browser.
|
||||
clearError();
|
||||
if (
|
||||
result.observedState
|
||||
&& isRecoveredPhysicalScanning(
|
||||
@@ -2882,7 +2903,7 @@ export function K1ProvisioningPipeline({
|
||||
|
||||
// Only an explicit public search owns the Step-1 Bluetooth loader.
|
||||
const searchActive = searchDisplayActive;
|
||||
const connectionEstablished = selectedModeConnected;
|
||||
const connectionEstablished = connectionPresentationEstablished;
|
||||
const connectionAttemptOwnsDraft = connectionAttemptSettling
|
||||
|| (
|
||||
connectionAttemptFailed
|
||||
@@ -3103,19 +3124,23 @@ export function K1ProvisioningPipeline({
|
||||
: "Переподключиться"}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
width="full"
|
||||
variant={
|
||||
connectionRecoveryTarget || connectionRecoveryVerificationPending
|
||||
? "secondary"
|
||||
: "primary"
|
||||
}
|
||||
icon={<Icon name="search" />}
|
||||
disabled={isBusy || modeResetInFlight}
|
||||
onClick={() => void changeDesiredConnectionMode(connectionMode)}
|
||||
>
|
||||
Подключить новый K1
|
||||
</Button>
|
||||
{!scenarioResetOwnsCleanDraft ? (
|
||||
<Button
|
||||
width="full"
|
||||
variant={
|
||||
connectionRecoveryTarget || connectionRecoveryVerificationPending
|
||||
? "secondary"
|
||||
: "primary"
|
||||
}
|
||||
icon={<Icon name="search" />}
|
||||
disabled={isBusy || modeResetInFlight}
|
||||
onClick={() => void changeDesiredConnectionMode(connectionMode)}
|
||||
>
|
||||
{savedBridgeNetworkSetupRequired
|
||||
? "Подключить K1 к общей сети"
|
||||
: "Подключить новый K1"}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
) : null;
|
||||
|
||||
@@ -3164,7 +3189,9 @@ export function K1ProvisioningPipeline({
|
||||
: presentedPhysicalRecoveryRequired
|
||||
? verificationActionPending
|
||||
? "Переподключение…"
|
||||
: "Нужно выбрать действие"
|
||||
: savedBridgeNetworkSetupRequired
|
||||
? "Нужна настройка сети"
|
||||
: "Нужно выбрать действие"
|
||||
: networkRecoveryRequired
|
||||
? appliedControlSettlementPending
|
||||
? "Подтверждение управления"
|
||||
@@ -3173,8 +3200,8 @@ export function K1ProvisioningPipeline({
|
||||
? connectionReconnectPending
|
||||
? "Переподключение…"
|
||||
: connectionRecoveryTarget
|
||||
? "Нужна проверка"
|
||||
: "Нужен новый поиск"
|
||||
? "Можно переподключиться"
|
||||
: "Подключить заново"
|
||||
: connectionEstablished
|
||||
? "Подключение установлено"
|
||||
: explicitProvisioningDraftStale
|
||||
@@ -3200,7 +3227,6 @@ export function K1ProvisioningPipeline({
|
||||
? "success"
|
||||
: networkRecoveryRequired
|
||||
|| presentedPhysicalRecoveryRequired
|
||||
|| presentedConnectionRecoveryRequired
|
||||
|| explicitProvisioningDraftStale
|
||||
? "warning"
|
||||
: "neutral"
|
||||
@@ -3239,15 +3265,20 @@ export function K1ProvisioningPipeline({
|
||||
aria-busy={physicalReconnectPending || undefined}
|
||||
>
|
||||
<div className="connection-summary" role="status">
|
||||
<span>Прежнее подключение не подтверждено</span>
|
||||
<span>
|
||||
{savedBridgeNetworkSetupRequired
|
||||
? "K1 не подключён к сохранённой общей сети"
|
||||
: "Прежнее подключение не подтверждено"}
|
||||
</span>
|
||||
<strong>{physicalRecoveryModeLabel}</strong>
|
||||
<small>
|
||||
{presentedPhysicalRecoveryDeviceId ?? "Прежний K1"}
|
||||
</small>
|
||||
</div>
|
||||
<p className="safety-note">
|
||||
Переподключитесь к прежнему K1 или начните чистое подключение
|
||||
нового устройства.
|
||||
{savedBridgeNetworkSetupRequired
|
||||
? "K1 ответил по Bluetooth, но не сообщил адрес Bridge. Настройки устройства не изменялись; требуется новое подключение к общей сети."
|
||||
: "Переподключитесь к прежнему K1 или начните чистое подключение нового устройства."}
|
||||
</p>
|
||||
{physicalReadOnlyVerificationAvailable ? (
|
||||
<Button
|
||||
@@ -3276,7 +3307,9 @@ export function K1ProvisioningPipeline({
|
||||
disabled={isBusy || modeResetInFlight}
|
||||
onClick={() => void changeDesiredConnectionMode(connectionMode)}
|
||||
>
|
||||
Подключить новый K1
|
||||
{savedBridgeNetworkSetupRequired
|
||||
? "Подключить K1 к общей сети"
|
||||
: "Подключить новый K1"}
|
||||
</Button>
|
||||
{candidateUnavailableMessage ? (
|
||||
<p className="safety-note" role="status">
|
||||
@@ -3289,7 +3322,8 @@ export function K1ProvisioningPipeline({
|
||||
className="field-stack"
|
||||
aria-busy={connectionReconnectPending || undefined}
|
||||
>
|
||||
{correlatedConnectionAttempt && error ? (
|
||||
{correlatedConnectionAttempt && error
|
||||
&& !savedBridgeNetworkSetupRequired ? (
|
||||
<K1OperatorError
|
||||
message={error}
|
||||
diagnostic={errorDiagnostic}
|
||||
@@ -3305,9 +3339,9 @@ export function K1ProvisioningPipeline({
|
||||
<>
|
||||
<div className="connection-summary" role="status">
|
||||
<span>
|
||||
{unknownNetworkOutcomeAttempt
|
||||
? "Результат применения сети не подтверждён"
|
||||
: "Сохранённое подключение требует проверки"}
|
||||
{savedBridgeNetworkSetupRequired
|
||||
? "K1 не подключён к сохранённой общей сети"
|
||||
: "Сохранённое подключение"}
|
||||
</span>
|
||||
<strong>
|
||||
{connectionRecoveryModeLabel
|
||||
@@ -3319,13 +3353,24 @@ export function K1ProvisioningPipeline({
|
||||
) : null}
|
||||
</div>
|
||||
<p className="safety-note">
|
||||
{unknownNetworkOutcomeAttempt
|
||||
? "Итог прежней попытки неизвестен. Автоматического повтора не было: сначала проверьте сохранённое подключение без изменений либо начните отдельный новый поиск."
|
||||
: "Проверка читает состояние прежнего K1 и не отправляет настройки сети, START или STOP. Новый поиск — отдельное явное действие для выбора другого устройства."}
|
||||
{savedBridgeNetworkSetupRequired
|
||||
? "K1 ответил по Bluetooth, но не сообщил адрес Bridge. Настройки устройства не изменялись; требуется новое подключение к общей сети."
|
||||
: "Переподключение использует сохранённый K1 без изменения сети. Новое подключение запускается отдельно через Bluetooth."}
|
||||
</p>
|
||||
{connectionRecoveryActions}
|
||||
</>
|
||||
)}
|
||||
{scenarioResetOwnsCleanDraft ? (
|
||||
<Button
|
||||
width="full"
|
||||
variant="secondary"
|
||||
icon={<Icon name="search" />}
|
||||
disabled={!canScan}
|
||||
onClick={() => void repeatDeviceScan()}
|
||||
>
|
||||
Найти по Bluetooth
|
||||
</Button>
|
||||
) : null}
|
||||
{candidateUnavailableMessage ? (
|
||||
<p className="safety-note" role="status">
|
||||
{candidateUnavailableMessage}
|
||||
@@ -3444,7 +3489,7 @@ export function K1ProvisioningPipeline({
|
||||
? "Настройка…"
|
||||
: explicitProvisioningDraftStale
|
||||
? "Результат устарел"
|
||||
: selectedModeConnected
|
||||
: connectionPresentationEstablished
|
||||
? "Готово"
|
||||
: "Ожидает настройки"
|
||||
}
|
||||
@@ -3459,7 +3504,7 @@ export function K1ProvisioningPipeline({
|
||||
? "accent"
|
||||
: networkRecoveryRequired || explicitProvisioningDraftStale
|
||||
? "warning"
|
||||
: selectedModeConnected
|
||||
: connectionPresentationEstablished
|
||||
? "success"
|
||||
: "neutral"
|
||||
}
|
||||
@@ -3558,7 +3603,7 @@ export function K1ProvisioningPipeline({
|
||||
<ActivityIndicator />
|
||||
<strong>Настройка сети…</strong>
|
||||
</div>
|
||||
) : selectedModeConnected && !changeNetworkDialogue ? (
|
||||
) : connectionPresentationEstablished && !changeNetworkDialogue ? (
|
||||
<div className="field-stack">
|
||||
<div className="connection-summary">
|
||||
<span>Подключение установлено</span>
|
||||
|
||||
Reference in New Issue
Block a user