feat(k1): add local connection matrix

This commit is contained in:
DCCONSTRUCTIONS
2026-07-19 09:52:22 +03:00
parent fecb5885d0
commit 57b2208cae
20 changed files with 1405 additions and 115 deletions
@@ -11,7 +11,7 @@ import {
type StatusTone,
} from "@nodedc/ui-react";
import { EXACT_PROFILE_SELECTION } from "../compatibility";
import { profileSelectionForConnectionMode } from "../compatibility";
import {
SUPPORTED_GNSS_MODE,
SUPPORTED_MOUNT_TYPE,
@@ -149,7 +149,9 @@ export function K1AcquisitionPipeline({
project_name: projectNameValidation.value,
mount_type: SUPPORTED_MOUNT_TYPE,
gnss_mode: SUPPORTED_GNSS_MODE,
compatibility_attestation: EXACT_PROFILE_SELECTION,
compatibility_attestation: profileSelectionForConnectionMode(
state.connection_mode ?? "bridge",
),
},
physicalAcceptance: PHYSICAL_ACCEPTANCE,
}),
@@ -11,9 +11,9 @@ import {
} from "@nodedc/ui-react";
import type { BleDevice } from "../api";
import { EXACT_PROFILE_SELECTION } from "../compatibility";
import { profileSelectionForConnectionMode } from "../compatibility";
import {
SUPPORTED_CONNECTION_MODE,
DEFAULT_CONNECTION_MODE,
connectionModeOptions,
type ConnectionMode,
} from "../configuration";
@@ -21,6 +21,36 @@ import { provisioningIntentKey } from "../lifecycle";
import { finiteMetric } from "../presentation";
import type { XgridsK1Controller } from "../runtimeContext";
const connectionCopy: Record<ConnectionMode, {
stepTitle: string;
ssidLabel: string;
ssidPlaceholder: string;
buttonLabel: string;
safetyNote: string;
}> = {
bridge: {
stepTitle: "Передайте настройки общей сети",
ssidLabel: "Название общей сети Wi‑Fi",
ssidPlaceholder: "Сеть локального контура",
buttonLabel: "Подключить K1 к общей сети",
safetyNote: "K1 получит реквизиты существующей сети одним рассмотренным BLE-запросом без автоматического повтора.",
},
"quick-connect": {
stepTitle: "Подключитесь к точке доступа K1",
ssidLabel: "Название точки доступа K1",
ssidPlaceholder: "SSID сканера, например XGR-…",
buttonLabel: "Подключить этот Mac к K1",
safetyNote: "Введите SSID и пароль точки доступа вашего K1. Mac сменит текущую Wi‑Fi сеть одним CoreWLAN-запросом; недокументированный BLE-секрет не читается и K1 не получает BLE-запись.",
},
"direct-connect": {
stepTitle: "Подключите K1 к хотспоту контроллера",
ssidLabel: "Название хотспота контроллера",
ssidPlaceholder: "SSID управляющего устройства",
buttonLabel: "Подключить K1 к хотспоту",
safetyNote: "Хотспот должен быть уже включён, а этот Mac — иметь к нему маршрут. K1 получит его реквизиты одним рассмотренным BLE-запросом.",
},
};
function WizardStep({
number,
title,
@@ -96,13 +126,17 @@ export function K1ProvisioningPipeline({
const [ssid, setSsid] = useState("");
const [password, setPassword] = useState("");
const [connectionMode, setConnectionMode] = useState<ConnectionMode>(
SUPPORTED_CONNECTION_MODE,
DEFAULT_CONNECTION_MODE,
);
const provisioningIntentRef = useRef<string | null>(null);
const devices = state?.devices ?? [];
const isBusy = pendingAction !== null;
const credentialsReady = 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,
);
useEffect(() => {
if (state?.selected_device_id) {
@@ -117,6 +151,12 @@ export function K1ProvisioningPipeline({
}
}, [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),
[devices, selectedDeviceId],
@@ -134,8 +174,8 @@ export function K1ProvisioningPipeline({
device_id: selectedDeviceId,
ssid: ssid.trim(),
password,
connection_mode: SUPPORTED_CONNECTION_MODE,
compatibility_attestation: EXACT_PROFILE_SELECTION,
connection_mode: connectionMode,
compatibility_attestation: profileSelectionForConnectionMode(connectionMode),
idempotency_key: idempotencyKey,
});
if (succeeded) {
@@ -152,7 +192,7 @@ export function K1ProvisioningPipeline({
</header>
<div className="configuration-anchor">
<span className="nodedc-field__description">
Неподтверждённые сетевые топологии уже отражены в интерфейсе, но не могут быть выбраны до отдельной приёмки.
Выберите направление связи. Каждый путь выполняет не более одного сетевого изменения и не повторяет его автоматически.
</span>
<Select
label="Способ подключения"
@@ -160,6 +200,8 @@ export function K1ProvisioningPipeline({
options={connectionModeOptions}
onChange={(value) => {
setConnectionMode(value);
setSsid("");
setPassword("");
resetProvisioningIntent();
}}
disabled={isBusy}
@@ -206,19 +248,19 @@ export function K1ProvisioningPipeline({
</WizardStep>
<WizardStep
number="03"
title="Передайте настройки общей сети"
status={state?.k1_ip ? "Адрес ранее получен" : "Настройки не переданы"}
tone={state?.k1_ip ? "warning" : "neutral"}
title={modeCopy.stepTitle}
status={selectedModeConnected ? "Адрес получен" : "Ожидает подключения"}
tone={selectedModeConnected ? "success" : "neutral"}
>
<div className="field-stack">
<TextField label="Название сети Wi‑Fi" hint="SSID" value={ssid} onChange={(event) => { setSsid(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" spellCheck={false} placeholder="Сеть локального контура" />
<TextField label={modeCopy.ssidLabel} hint="SSID" value={ssid} onChange={(event) => { setSsid(event.target.value); provisioningIntentRef.current = null; }} autoComplete="off" spellCheck={false} placeholder={modeCopy.ssidPlaceholder} />
<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>
<Button width="full" variant="primary" icon={<Icon name="network" />} disabled={!canConnect} onClick={() => void submitConnect()}>
{pendingAction === "connect" ? "Подключаем…" : "Подключить устройство к Wi‑Fi"}
{pendingAction === "connect" ? "Подключаем…" : modeCopy.buttonLabel}
</Button>
<p className="safety-note">Наличие адреса подтверждает результат предыдущей настройки, но не текущее соединение. Пароль передаётся только локальному сервису, не сохраняется в браузере и удаляется из формы после успеха.</p>
<p className="safety-note">{modeCopy.safetyNote} Пароль передаётся только локальному сервису, не сохраняется в браузере и удаляется из формы после успеха.</p>
</WizardStep>
</div>
</GlassSurface>