chore(node): preserve pre-canonicalization experiment snapshot

Historical working copy retained for audit before consolidation into main. The canonicalized plugin architecture and later fixes already live in main; this snapshot is not a release or a request to restore obsolete source layout.
This commit is contained in:
DCCONSTRUCTIONS
2026-09-21 08:45:34 +03:00
parent 020a878915
commit 1c7dd29d8a
89 changed files with 5026 additions and 487 deletions
@@ -275,6 +275,11 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
: sourceRuntimeBusy || preparedAcquisition
? "warning"
: "neutral";
const connectionAttemptFailed = !sourceRuntimeBusy && !preparedAcquisition
&& connectionTopology?.status !== "active"
&& state?.connection_attempt?.connection_mode === effectiveDesiredConnectionMode
&& ["failed", "cancelled", "timed_out", "interrupted", "operator_action_required"]
.includes(state.connection_attempt.status);
const connectionControlBootstrapSettling =
isConnectionControlBootstrapSettling(state);
const connectionPhaseLabel = livePreparationPending
@@ -293,6 +298,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
? savedBridgeNetworkSetupRequired
? "Нужна настройка сети"
: "Требуется действие"
: connectionAttemptFailed
? "Подключение не завершено"
: projectedPhase === "error"
? connectionPhaseFallbackLabel(projectedPhase)
: connectionTopology?.status === "active"
@@ -316,6 +323,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
? "accent"
: physicalRecoveryRequired
? "warning"
: connectionAttemptFailed
? "warning"
: projectedPhase === "error"
? phaseTone(projectedPhase)
: connectionTopology?.status === "active"
@@ -336,6 +345,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
: physicalRecoveryRequired
? physicalRecoveryDetail
?? "Безопасное восстановление прежнего K1 сейчас недоступно."
: connectionAttemptFailed
? "Проверьте состояние K1 или начните новое подключение."
: !sourceRuntimeBusy && connectionTopology?.status === "configured-unverified"
? "Начните новое подключение."
: !sourceRuntimeBusy && connectionTopology?.status === "active"
@@ -3,13 +3,14 @@ import { Button } from "@nodedc/ui-react";
import type { XgridsConnectionAttempt } from "../api";
import { hostFailureDiagnosticPresentation } from "../hostDiagnosticPresentation";
import { STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation";
const connectionAttemptStageLabels: Record<string, string> = {
accepted: "Запрос принят",
"scan-selection-admitted": "Результат выбран",
"host-wifi-profile-preflight": "Подготовка профиля Wi‑Fi",
"device-ap-activation": "Подготовка локальной сети",
"ble-provisioning-write": ередаются настройки сети",
"ble-provisioning-write": одключение K1 к Wi‑Fi",
"ble-write-dispatched": "Настройки переданы",
"status-observing": "Ожидание ответа",
"device-topology-applied": "Целевая сеть подтверждена",
@@ -74,6 +75,9 @@ export function attemptNextActionLabel(
}
const publicConnectionErrorLabels: Readonly<Record<string, string>> = {
...STATION_WIFI_FAILURE_MESSAGES,
BleakGATTProtocolError:
"K1 ответил ошибкой Bluetooth. Подключение к Wi‑Fi не подтверждено; проверьте состояние устройства или начните новое подключение.",
"network-provision-discovery-generation-conflict":
"Результат Bluetooth-поиска устарел до отправки. Настройки устройства не изменялись; выполните новый поиск.",
"connection-mode-draft-revision-conflict":
@@ -31,6 +31,7 @@ import type {
} from "../api";
import { profileSelectionForConnectionMode } from "../compatibility";
import {
BLE_DISCOVERY_TIMEOUT_SECONDS,
DEFAULT_CONNECTION_MODE,
connectionModeOptions,
type ConnectionMode,
@@ -66,10 +67,12 @@ import {
type ReadOnlyConnectionObservationTarget,
} from "../lifecycle";
import { finiteMetric } from "../presentation";
import { STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation";
import type { XgridsK1Controller } from "../runtimeContext";
import { K1OperatorError } from "./K1OperatorError";
import {
connectionActionAuthoritySnapshot,
networkProvisionFailureMessage,
type BleDiscoverySubmitResult,
type ConnectionActionAuthoritySnapshot,
} from "../useXgridsK1Runtime";
@@ -1866,7 +1869,7 @@ export function K1ProvisioningPipeline({
const connectionRecoveryRequired = connectionRecoveryIsRequired(
connectionRecoveryKey,
escapedConnectionRecoveryKey,
) && !(searchRequested && !searchDisplayActive);
) && (connectionAttemptFailed || !(searchRequested && !searchDisplayActive));
const presentedConnectionRecoveryRequired = !livePreparationPending && (
connectionRecoveryRequired || connectionRecoveryVerificationPending
);
@@ -2089,7 +2092,7 @@ export function K1ProvisioningPipeline({
const startedAt = Date.now();
const updateCountdown = () => {
const elapsedSeconds = Math.floor((Date.now() - startedAt) / 1_000);
setScanSecondsRemaining(Math.max(0, 6 - elapsedSeconds));
setScanSecondsRemaining(Math.max(0, BLE_DISCOVERY_TIMEOUT_SECONDS - elapsedSeconds));
};
updateCountdown();
const timer = window.setInterval(updateCountdown, 250);
@@ -2350,7 +2353,7 @@ export function K1ProvisioningPipeline({
let scanResult: BleDiscoverySubmitResult | null = null;
try {
if (!runtimeActionIsCurrent(actionFence)) return;
scanResult = await scanWithResult({ durationSeconds: 6 });
scanResult = await scanWithResult({ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS });
if (
!localScenarioActionEpochIsCurrent(
actionEpoch,
@@ -2573,12 +2576,12 @@ export function K1ProvisioningPipeline({
}
const freshStartAllowed = result.intentDisposition === "release";
const failureMessage = freshStartAllowed
const failureMessage = networkProvisionFailureMessage(observedOperation) ?? (freshStartAllowed
&& provisioningFailureRequiresFreshCandidate(result.failureReasonCode)
? "Результат Bluetooth-поиска устарел до отправки. Настройки не отправлены; выполните явный повторный поиск."
: freshStartAllowed
? "Подключение не началось. Настройки удалены; после устранения причины начните новый явный поиск."
: "Результат отправки неизвестен. Повторное применение заблокировано, чтобы не отправить BLE-команду дважды; дождитесь, пока система определит безопасное продолжение.";
: "Подключение не подтверждено. Проверьте состояние K1 или начните новое подключение; автоматического повтора не было.");
setConnectionAttemptPresentation((current) => (
current?.idempotencyKey === idempotencyKey
? {
@@ -3203,7 +3206,9 @@ export function K1ProvisioningPipeline({
disabled={isBusy || modeResetInFlight}
onClick={() => void changeDesiredConnectionMode(connectionMode)}
>
{savedBridgeNetworkSetupRequired
{STATION_WIFI_FAILURE_MESSAGES[connectionRecoveryAttempt?.public_error_code ?? ""]
? "Указать сеть Wi‑Fi заново"
: savedBridgeNetworkSetupRequired
? "Подключить K1 к общей сети"
: "Подключить новый K1"}
</Button>
@@ -3256,7 +3261,7 @@ export function K1ProvisioningPipeline({
title="Подключение"
status={
searchActive
? `Поиск · ${scanSecondsRemaining ?? 6} с`
? `Поиск · до ${scanSecondsRemaining ?? BLE_DISCOVERY_TIMEOUT_SECONDS} с`
: connectionAttemptSettling
? "Устройство выбрано"
: presentedPhysicalRecoveryRequired
@@ -3316,7 +3321,7 @@ export function K1ProvisioningPipeline({
>
<ActivityIndicator />
<strong>
Поиск Bluetooth · {scanSecondsRemaining ?? 6} с
Поиск Bluetooth · до {scanSecondsRemaining ?? BLE_DISCOVERY_TIMEOUT_SECONDS} с
</strong>
</div>
) : networkRecoveryRequired
@@ -4,6 +4,9 @@ export type ConnectionMode = "bridge" | "quick-connect" | "direct-connect";
export type MountType = "handheld" | "vehicle-mounted" | "uav" | "backpack";
export type GnssMode = "none" | "rtk" | "ppk";
// Upper bound for one continuous discovery session; it can finish earlier when K1 is visible.
export const BLE_DISCOVERY_TIMEOUT_SECONDS = 20;
export const DEFAULT_CONNECTION_MODE = "bridge" as const satisfies ConnectionMode;
export const SUPPORTED_MOUNT_TYPE = "handheld" as const satisfies MountType;
export const SUPPORTED_GNSS_MODE = "none" as const satisfies GnssMode;
@@ -0,0 +1,7 @@
/** Reviewed K1 station replies; Quick Connect host errors have separate copy. */
export const STATION_WIFI_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
"k1-wifi-network-not-found":
"K1 не смог найти указанную сеть Wi‑Fi. Проверьте точное название сети и её доступность рядом с K1. Для новой попытки выберите K1 через поиск Bluetooth и укажите сеть заново.",
"k1-wifi-credentials-required":
"K1 не смог подключиться с переданным паролем Wi‑Fi. Для новой попытки выберите K1 через поиск Bluetooth и введите пароль нужной сети заново.",
};
@@ -0,0 +1,241 @@
import { ApiError, type ConnectRequest, type XgridsK1State, type XgridsOperation } from "./api";
import { operationByIdempotencyKey } from "./lifecycle";
import { selectMonotonicXgridsState } from "./stateOrdering";
import { STATION_WIFI_FAILURE_MESSAGES } from "./networkFailurePresentation";
const CONTROL_STATE_READ_INTERVAL_MS = 250;
const NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS = 30_000;
const NETWORK_PROVISION_SETTLEMENT_MAX_MS = 300_000;
const NETWORK_PROVISION_SETTLEMENT_GRACE_MS = 1_000;
function networkProvisionSettlementDeadline(
operation: XgridsOperation,
startedAtMs: number,
): number {
const operationDeadlineMs = operation.deadline_at
? Date.parse(operation.deadline_at)
: Number.NaN;
const requestedDeadlineMs = Number.isFinite(operationDeadlineMs)
&& operationDeadlineMs > startedAtMs
? operationDeadlineMs + NETWORK_PROVISION_SETTLEMENT_GRACE_MS
: startedAtMs + NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS;
return Math.min(
requestedDeadlineMs,
startedAtMs + NETWORK_PROVISION_SETTLEMENT_MAX_MS,
);
}
/**
* Follow one already-admitted Apply after a browser response is interrupted by
* the host Wi-Fi handoff. This loop only reads the local journal; it never
* retries the HTTP mutation, BLE write, CoreWLAN association or control open.
*/
export async function awaitNetworkProvisionSettlementAfterLostResponse(
initialState: XgridsK1State,
idempotencyKey: string,
readState: () => Promise<XgridsK1State>,
acceptState: (state: XgridsK1State) => void,
assertOperatorIntentCurrent: () => void,
options: {
now?: () => number;
wait?: (delayMs: number) => Promise<void>;
} = {},
): Promise<XgridsK1State> {
const now = options.now ?? Date.now;
const wait = options.wait ?? ((delayMs: number) => new Promise<void>((resolve) => {
globalThis.setTimeout(resolve, delayMs);
}));
let state = initialState;
let operation = operationByIdempotencyKey(
state,
"network.provision",
idempotencyKey,
);
if (!operation || !["accepted", "running"].includes(operation.status)) {
return state;
}
const deadlineMs = networkProvisionSettlementDeadline(operation, now());
while (["accepted", "running"].includes(operation.status)) {
assertOperatorIntentCurrent();
const remainingMs = deadlineMs - now();
if (remainingMs <= 0) return state;
await wait(Math.min(CONTROL_STATE_READ_INTERVAL_MS, remainingMs));
assertOperatorIntentCurrent();
try {
const nextState = await readState();
assertOperatorIntentCurrent();
acceptState(nextState);
state = nextState;
} catch (readError) {
// A host Wi-Fi transition can briefly abort even a localhost fetch.
// Preserve the admitted operation and perform only the next bounded
// journal read; the original Apply is never reissued.
if (!(readError instanceof ApiError) || !readError.transportUnavailable) {
throw readError;
}
continue;
}
operation = operationByIdempotencyKey(
state,
"network.provision",
idempotencyKey,
);
if (!operation) return state;
}
return state;
}
export interface ProvisioningObservation {
state: XgridsK1State;
operation: XgridsOperation | null;
/** HTTP failure is subordinate to the journal; it never authorizes a retry. */
transportError: unknown;
}
/**
* One explicit intent: submit at most once, then observe its exact journal row.
* Bluetooth discovery, host WLAN changes and the vendor control dialogue stay
* server-owned. No error path can resubmit or silently choose another device.
*/
export async function observeProvisioningRequest(
request: ConnectRequest,
context: {
initialState: XgridsK1State | null;
send: (request: ConnectRequest) => Promise<XgridsK1State>;
readState: () => Promise<XgridsK1State>;
acceptState: (state: XgridsK1State) => void;
currentState: () => XgridsK1State | null;
assertCurrent: () => void;
settlementOptions?: Parameters<typeof awaitNetworkProvisionSettlementAfterLostResponse>[5];
},
): Promise<ProvisioningObservation> {
let state = context.initialState;
let transportError: unknown = null;
const adopt = (incoming: XgridsK1State): XgridsK1State => {
context.acceptState(incoming);
context.assertCurrent();
state = selectMonotonicXgridsState(context.currentState(), incoming);
return state;
};
context.assertCurrent();
const existing = operationByIdempotencyKey(state, "network.provision", request.idempotency_key);
if (!existing) {
let response: XgridsK1State;
try {
response = await context.send(request);
} catch (error) {
transportError = error;
context.assertCurrent();
try {
response = await context.readState();
} catch {
throw error;
}
}
adopt(response);
}
if (!state) throw new ApiError("Состояние подключения недоступно. Обновите экран перед новым действием.");
const settled = await awaitNetworkProvisionSettlementAfterLostResponse(
state,
request.idempotency_key,
async () => adopt(await context.readState()),
adopt,
context.assertCurrent,
context.settlementOptions,
);
state = adopt(settled);
return {
state,
operation: operationByIdempotencyKey(state, "network.provision", request.idempotency_key),
transportError,
};
}
const CONNECT_FAILURE_NEXT_STEP_COPY =
"Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.";
function networkFailureNextStepMessage(detail: string): string {
return `${detail} ${CONNECT_FAILURE_NEXT_STEP_COPY}`;
}
export function networkProvisionFailureMessage(
operation: XgridsOperation | null | undefined,
): string | null {
if (!operation || operation.status !== "failed") return null;
const code = operation.error?.code;
if (typeof code !== "string") return null;
if (STATION_WIFI_FAILURE_MESSAGES[code]) return STATION_WIFI_FAILURE_MESSAGES[code];
if (code === "BleakGATTProtocolError") {
const attCode = operation.error?.ble_att_error_code;
const attName = operation.error?.ble_att_error_name;
const attDetail = typeof attCode === "number" && typeof attName === "string"
? ` Код периферии: ATT ${attCode} ${attName}.`
: "";
if (operation.error?.device_write_attempted === false) {
return `Bluetooth-сеанс K1 завершился ошибкой до команды изменения сети.${attDetail} Запись сетевого профиля не выполнялась; выполните новый поиск после освобождения Bluetooth.`;
}
if (typeof attCode === "number" && typeof attName === "string") {
return networkFailureNextStepMessage(
`Bluetooth-периферия завершила сетевую операцию ошибкой.${attDetail} Команда могла быть принята K1; итог текущей попытки не подтверждён.`,
);
}
return networkFailureNextStepMessage(
"Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён.",
);
}
if (code === "network-not-found") {
const attemptCount = operation.error?.scan_attempt_count;
const elapsedMs = operation.error?.scan_elapsed_ms;
const scanDetail = typeof attemptCount === "number" && typeof elapsedMs === "number"
? ` macOS выполнила ${attemptCount} проверок за ${(elapsedMs / 1000).toFixed(1)} с.`
: "";
return `K1 принял команду Quick Connect и подтвердил готовность точки доступа, но macOS не увидела её Wi‑Fi-сеть за отведённое время.${scanDetail} Quick Connect не установлен; автоматического повтора не было. Выполните новый поиск перед следующей явной попыткой или используйте Bridge.`;
}
if (
code === "keychain-authorization-required"
|| code === "keychain-authorization-denied"
|| code === "keychain-authorization-cancelled"
|| code === "keychain-access-failed"
) {
const failedBeforeDeviceWrite = operation.error?.side_effect_status === "none";
return failedBeforeDeviceWrite
? "Локальный профиль K1 недоступен в связке ключей. Команда устройству не отправлялась; подготовьте разрешение профиля отдельным действием и затем повторите подключение."
: networkFailureNextStepMessage(
"После подтверждённого включения точки K1 локальный профиль стал недоступен в связке ключей. Дополнительный пароль не запрашивался; итог текущей попытки не подтверждён.",
);
}
const messages: Record<string, string> = {
"host-wifi-operation-timeout":
networkFailureNextStepMessage("Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён."),
"profile-ssid-mismatch":
"Сохранённый профиль относится к другому устройству. Подключение остановлено без повторной команды сканеру.",
"profile-credential-source-mismatch":
"Сохранённый профиль K1 не подтверждён для точной версии прошивки. Автоматического выбора другого пароля нет; подготовьте профиль отдельно перед новой попыткой.",
"profile-unavailable":
networkFailureNextStepMessage("Локальный профиль выбранного K1 отсутствует; текущая попытка подключения завершилась ошибкой."),
"corewlan-error":
"macOS не смогла подключиться к точке доступа K1. Проверьте пароль сохранённой сети этого K1; автоматического повтора не было.",
"wifi-interface-unavailable":
"Системный Wi-Fi-интерфейс macOS недоступен. Команда сканеру автоматически не повторялась.",
"unsupported-platform":
"Для этой операционной системы адаптер подключения к точке K1 ещё не реализован.",
"credential-source-unavailable":
"Локальный профиль выбранного K1 не готов. Команда устройству не отправлялась; после подготовки профиля разрешена новая явная попытка.",
"network-provision-candidate-not-fresh":
"Результат Bluetooth-поиска отсутствует или устарел. Команда K1 не отправлялась; выполните один свежий поиск.",
"network-provision-candidate-changed":
"Bluetooth-кандидат изменился до команды K1. Записи не было; выполните один свежий поиск.",
"network-provision-candidate-name-unavailable":
"K1 не сообщил имя своей точки доступа. Команда устройству не отправлялась; выполните новый поиск.",
"network-provision-target-not-distinguishable-from-baseline":
networkFailureNextStepMessage("После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён."),
"network-provision-lifecycle-busy":
"Сетевая операция заблокирована активной сессией или локальной очисткой. Команда K1 не отправлялась; завершите текущую сессию и повторите явно.",
};
return messages[code] ?? null;
}
@@ -90,9 +90,9 @@ const connectionPolicyNextActionCopy: Record<string, string> = {
"diagnose-physical-command-ledger": "Не повторяйте команду; сначала проверьте журнал физических команд.",
"restart-ble-runtime": "Контролируемо перезапустите локальный BLE-контур и обновите состояние.",
"scan-ble": "Выполните свежий поиск Bluetooth-устройств.",
"observe-fresh-device-network": "Дождитесь автоматического восстановления связи с выбранным K1.",
"observe-current-device-network": "Дождитесь автоматического восстановления связи с тем же K1.",
"observe-configured-device-network": "Дождитесь автоматического восстановления сохранённого подключения K1.",
"observe-fresh-device-network": "Нажмите «Переподключиться», чтобы проверить состояние выбранного K1.",
"observe-current-device-network": "Нажмите «Переподключиться», чтобы проверить связь с тем же K1.",
"observe-configured-device-network": "Нажмите «Переподключиться», чтобы проверить сохранённое подключение K1.",
"recover-current-device-network": "Нажмите «Подключиться заново».",
"inspect-host-network": "Проверьте активную локальную сеть и маршрут этого компьютера.",
"probe-endpoint": "Дождитесь обновления подключения K1.",
@@ -39,8 +39,6 @@ import {
newMutationContext,
newOperationId,
operationAllowsFreshProvisioningIntent,
operationByIdempotencyKey,
operationNeedsReconciliation,
physicalStopIntentCheckpoint,
recommendedConnectionRecoveryObservationTarget,
readOnlyVerificationClearedReconciliation,
@@ -65,7 +63,9 @@ import {
import { selectMonotonicXgridsState } from "./stateOrdering";
import { operationHostFailureDiagnostic } from "./hostDiagnosticPresentation";
import { activeStreamForceFinishAuthority } from "./activeStreamRecovery";
import { DEFAULT_CONNECTION_MODE } from "./configuration";
import { BLE_DISCOVERY_TIMEOUT_SECONDS, DEFAULT_CONNECTION_MODE } from "./configuration";
import { observeProvisioningRequest, networkProvisionFailureMessage } from "./networkProvisioning";
export { awaitNetworkProvisionSettlementAfterLostResponse, networkProvisionFailureMessage } from "./networkProvisioning";
export type PendingAction =
| "scan"
@@ -265,87 +265,6 @@ export function connectionActionAuthoritySnapshot(
}
const CONTROL_STATE_READ_INTERVAL_MS = 250;
const NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS = 30_000;
const NETWORK_PROVISION_SETTLEMENT_MAX_MS = 300_000;
const NETWORK_PROVISION_SETTLEMENT_GRACE_MS = 1_000;
function networkProvisionSettlementDeadline(
operation: XgridsOperation,
startedAtMs: number,
): number {
const operationDeadlineMs = operation.deadline_at
? Date.parse(operation.deadline_at)
: Number.NaN;
const requestedDeadlineMs = Number.isFinite(operationDeadlineMs)
&& operationDeadlineMs > startedAtMs
? operationDeadlineMs + NETWORK_PROVISION_SETTLEMENT_GRACE_MS
: startedAtMs + NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS;
return Math.min(
requestedDeadlineMs,
startedAtMs + NETWORK_PROVISION_SETTLEMENT_MAX_MS,
);
}
/**
* Follow one already-admitted Apply after a browser response is interrupted by
* the host Wi-Fi handoff. This loop only reads the local journal; it never
* retries the HTTP mutation, BLE write, CoreWLAN association or control open.
*/
export async function awaitNetworkProvisionSettlementAfterLostResponse(
initialState: XgridsK1State,
idempotencyKey: string,
readState: () => Promise<XgridsK1State>,
acceptState: (state: XgridsK1State) => void,
assertOperatorIntentCurrent: () => void,
options: {
now?: () => number;
wait?: (delayMs: number) => Promise<void>;
} = {},
): Promise<XgridsK1State> {
const now = options.now ?? Date.now;
const wait = options.wait ?? ((delayMs: number) => new Promise<void>((resolve) => {
globalThis.setTimeout(resolve, delayMs);
}));
let state = initialState;
let operation = operationByIdempotencyKey(
state,
"network.provision",
idempotencyKey,
);
if (!operation || !["accepted", "running"].includes(operation.status)) {
return state;
}
const deadlineMs = networkProvisionSettlementDeadline(operation, now());
while (["accepted", "running"].includes(operation.status)) {
assertOperatorIntentCurrent();
const remainingMs = deadlineMs - now();
if (remainingMs <= 0) return state;
await wait(Math.min(CONTROL_STATE_READ_INTERVAL_MS, remainingMs));
assertOperatorIntentCurrent();
try {
const nextState = await readState();
assertOperatorIntentCurrent();
acceptState(nextState);
state = nextState;
} catch (readError) {
// A host Wi-Fi transition can briefly abort even a localhost fetch.
// Preserve the admitted operation and perform only the next bounded
// journal read; the original Apply is never reissued.
if (!(readError instanceof ApiError) || !readError.transportUnavailable) {
throw readError;
}
continue;
}
operation = operationByIdempotencyKey(
state,
"network.provision",
idempotencyKey,
);
if (!operation) return state;
}
return state;
}
const CONTROL_PHASE_WAIT_TIMEOUT_MS = 120_000;
function controlPhase(state: XgridsK1State): XgridsApplicationControlPhase {
@@ -642,93 +561,6 @@ function apiErrorForOperation(
);
}
const CONNECT_SESSION_RESET_COPY =
"Автоматический повтор команды K1 не отправлялся. Сессия подключения в интерфейсе сброшена. Выполните новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз.";
function resetConnectSessionMessage(detail: string): string {
return `${detail} ${CONNECT_SESSION_RESET_COPY}`;
}
export function networkProvisionFailureMessage(
operation: XgridsOperation | null | undefined,
): string | null {
if (!operation || operation.status !== "failed") return null;
const code = operation.error?.code;
if (typeof code !== "string") return null;
if (code === "BleakGATTProtocolError") {
const attCode = operation.error?.ble_att_error_code;
const attName = operation.error?.ble_att_error_name;
const attDetail = typeof attCode === "number" && typeof attName === "string"
? ` Код периферии: ATT ${attCode} ${attName}.`
: "";
if (operation.error?.device_write_attempted === false) {
return `Bluetooth-сеанс K1 завершился ошибкой до команды изменения сети.${attDetail} Запись сетевого профиля не выполнялась; выполните новый поиск после освобождения Bluetooth.`;
}
if (typeof attCode === "number" && typeof attName === "string") {
return resetConnectSessionMessage(
`Bluetooth-периферия завершила сетевую операцию ошибкой.${attDetail} Команда могла быть принята K1; итог текущей попытки не подтверждён.`,
);
}
return resetConnectSessionMessage(
"Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён.",
);
}
if (code === "network-not-found") {
const attemptCount = operation.error?.scan_attempt_count;
const elapsedMs = operation.error?.scan_elapsed_ms;
const scanDetail = typeof attemptCount === "number" && typeof elapsedMs === "number"
? ` macOS выполнила ${attemptCount} проверок за ${(elapsedMs / 1000).toFixed(1)} с.`
: "";
return `K1 принял команду Quick Connect и подтвердил готовность точки доступа, но macOS не увидела её Wi‑Fi-сеть за отведённое время.${scanDetail} Quick Connect не установлен; автоматического повтора не было. Выполните новый поиск перед следующей явной попыткой или используйте Bridge.`;
}
if (
code === "keychain-authorization-required"
|| code === "keychain-authorization-denied"
|| code === "keychain-authorization-cancelled"
|| code === "keychain-access-failed"
) {
const failedBeforeDeviceWrite = operation.error?.side_effect_status === "none";
return failedBeforeDeviceWrite
? "Локальный профиль K1 недоступен в связке ключей. Команда устройству не отправлялась; подготовьте разрешение профиля отдельным действием и затем повторите подключение."
: resetConnectSessionMessage(
"После подтверждённого включения точки K1 локальный профиль стал недоступен в связке ключей. Дополнительный пароль не запрашивался; итог текущей попытки не подтверждён.",
);
}
const messages: Record<string, string> = {
"host-wifi-operation-timeout":
resetConnectSessionMessage("Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён."),
"profile-ssid-mismatch":
"Сохранённый профиль относится к другому устройству. Подключение остановлено без повторной команды сканеру.",
"profile-credential-source-mismatch":
"Сохранённый профиль K1 не подтверждён для точной версии прошивки. Автоматического выбора другого пароля нет; подготовьте профиль отдельно перед новой попыткой.",
"profile-unavailable":
resetConnectSessionMessage("Локальный профиль выбранного K1 отсутствует; текущая попытка подключения завершилась ошибкой."),
"corewlan-error":
"macOS не смогла подключиться к точке доступа K1. Проверьте пароль сохранённой сети этого K1; автоматического повтора не было.",
"wifi-interface-unavailable":
"Системный Wi-Fi-интерфейс macOS недоступен. Команда сканеру автоматически не повторялась.",
"unsupported-platform":
"Для этой операционной системы адаптер подключения к точке K1 ещё не реализован.",
"credential-source-unavailable":
"Локальный профиль выбранного K1 не готов. Команда устройству не отправлялась; после подготовки профиля разрешена новая явная попытка.",
"network-provision-candidate-not-fresh":
"Результат Bluetooth-поиска отсутствует или устарел. Команда K1 не отправлялась; выполните один свежий поиск.",
"network-provision-candidate-changed":
"Bluetooth-кандидат изменился до команды K1. Записи не было; выполните один свежий поиск.",
"network-provision-candidate-name-unavailable":
"K1 не сообщил имя своей точки доступа. Команда устройству не отправлялась; выполните новый поиск.",
"network-provision-target-not-distinguishable-from-baseline":
resetConnectSessionMessage("После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён."),
"network-provision-lifecycle-busy":
"Сетевая операция заблокирована активной сессией или локальной очисткой. Команда K1 не отправлялась; завершите текущую сессию и повторите явно.",
};
return messages[code] ?? null;
}
export function discoveryScanFailureMessage(
operation: XgridsOperation | null | undefined,
): string | null {
@@ -1138,7 +970,7 @@ export function useXgridsK1Runtime(enabled: boolean) {
const operationId = newOperationId();
try {
const nextState = await xgridsK1Api.scanBle({
duration_seconds: options.durationSeconds ?? 6,
duration_seconds: options.durationSeconds ?? BLE_DISCOVERY_TIMEOUT_SECONDS,
operation_id: operationId,
expected_snapshot_runtime_id: expectedSnapshotRuntimeId(),
});
@@ -1412,143 +1244,36 @@ export function useXgridsK1Runtime(enabled: boolean) {
);
}
};
const previous = operationByIdempotencyKey(
state,
"network.provision",
request.idempotency_key,
);
if (operationNeedsReconciliation(previous)) {
captureFailureReason(previous);
throw apiErrorForOperation(
resetConnectSessionMessage(
"Текущая попытка подключения не получила подтверждённого результата.",
),
previous,
);
}
if (operationAllowsFreshProvisioningIntent(previous)) {
intentDisposition = "release";
captureFailureReason(previous);
const failureMessage = networkProvisionFailureMessage(previous);
throw apiErrorForOperation(
failureMessage
?? "Подготовительный этап завершился до команды K1. После устранения причины разрешена новая явная попытка.",
previous,
);
}
let nextState: XgridsK1State;
if (previous?.status === "succeeded" && state) {
nextState = state;
} else try {
nextState = await xgridsK1Api.connect({
...request,
const observation = await observeProvisioningRequest(request, {
initialState: latestState.current,
send: (input) => xgridsK1Api.connect({
...input,
expected_snapshot_runtime_id: expectedSnapshotRuntimeId(),
});
} catch (connectError) {
// A failed network action is terminal and persisted by the backend.
// Re-read state only; never replay the device command. This replaces
// an opaque HTTP 502 banner with the exact host-association outcome.
let failedState: XgridsK1State;
try {
failedState = await xgridsK1Api.getState();
} catch {
throw connectError;
}
acceptState(failedState);
failedState = await awaitNetworkProvisionSettlementAfterLostResponse(
failedState,
request.idempotency_key,
() => xgridsK1Api.getState(),
acceptState,
assertOperatorIntentCurrent,
);
const failedOperation = operationByIdempotencyKey(
failedState,
"network.provision",
request.idempotency_key,
);
// The response body can be lost after the backend has already
// committed the exact idempotent operation. The read-only journal is
// authoritative: acknowledge that success instead of presenting a
// false failure which could invite another operator click.
if (failedOperation?.status === "succeeded") {
nextState = failedState;
if (
!exactAppliedNetworkIntentCompleted(
nextState,
request,
failedOperation,
)
&& !hasExactConnectionReady(nextState, request.connection_mode)
) {
throw connectError;
}
} else {
captureFailureReason(failedOperation);
if (operationNeedsReconciliation(failedOperation)) {
throw apiErrorForOperation(
resetConnectSessionMessage(
"Текущая попытка подключения завершилась с неизвестным результатом.",
),
failedOperation,
);
}
if (operationAllowsFreshProvisioningIntent(failedOperation)) {
intentDisposition = "release";
const failureMessage = networkProvisionFailureMessage(failedOperation);
throw apiErrorForOperation(
failureMessage
?? "Подготовительный этап завершился до команды K1. После устранения причины разрешена новая явная попытка.",
failedOperation,
);
}
const failureMessage = networkProvisionFailureMessage(failedOperation);
if (failureMessage) {
throw apiErrorForOperation(failureMessage, failedOperation);
}
if (failedOperation) {
throw apiErrorForOperation(
resetConnectSessionMessage(
"Текущая попытка подключения не получила подтверждённого результата.",
),
failedOperation,
);
}
throw connectError;
}
}
const operation = operationByIdempotencyKey(
nextState,
"network.provision",
request.idempotency_key,
);
if (operationNeedsReconciliation(operation)) {
captureFailureReason(operation);
throw apiErrorForOperation(
resetConnectSessionMessage(
"Текущая попытка подключения завершилась с неизвестным результатом.",
),
operation,
);
}
if (operationAllowsFreshProvisioningIntent(operation)) {
intentDisposition = "release";
}),
readState: () => xgridsK1Api.getState(),
acceptState,
currentState: () => latestState.current,
assertCurrent: assertOperatorIntentCurrent,
});
// Return the same authoritative snapshot on failure and success. The
// form must not reconstruct an outcome from an empty result object.
observedState = observation.state;
let nextState = observation.state;
const operation = observation.operation;
if (operation?.status !== "succeeded") {
captureFailureReason(operation);
intentDisposition = operationAllowsFreshProvisioningIntent(operation)
? "release"
: "retain";
const failureMessage = networkProvisionFailureMessage(operation);
throw apiErrorForOperation(
failureMessage
?? "Подготовительный этап завершился до команды K1. После устранения причины разрешена новая явная попытка.",
operation,
);
}
if (operation && operation.status !== "succeeded") {
captureFailureReason(operation);
throw apiErrorForOperation(
resetConnectSessionMessage(
"Текущая попытка подключения завершилась без подтверждения успеха.",
),
operation,
if (operation) {
throw apiErrorForOperation(
failureMessage ?? "Подключение не подтверждено. Проверьте состояние K1 или начните новое подключение; автоматического повтора не было.",
operation,
);
}
throw observation.transportError ?? new ApiError(
"Сервис не подтвердил результат подключения. Обновите состояние; команда автоматически не повторяется.",
);
}
const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted(