Unify K1 discovery ownership and verification across enrollment paths

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 20:04:46 +03:00
parent 9c4d70b0e9
commit 762d77ef95
27 changed files with 615 additions and 191 deletions
@@ -3,7 +3,7 @@ import { Button } from "@nodedc/ui-react";
import type { XgridsConnectionAttempt } from "../api";
import { hostFailureDiagnosticPresentation } from "../hostDiagnosticPresentation";
import { STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation";
import { BLE_DISCOVERY_FAILURE_MESSAGES, STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation";
const connectionAttemptStageLabels: Record<string, string> = {
accepted: "Запрос принят",
@@ -97,6 +97,7 @@ function publicConnectionErrorLabel(
structured: ReturnType<typeof hostFailureDiagnosticPresentation>,
): string {
const publicCode = attempt?.public_error_code?.trim();
if (attempt?.side_effect_status === "none" && publicCode && BLE_DISCOVERY_FAILURE_MESSAGES[publicCode]) return BLE_DISCOVERY_FAILURE_MESSAGES[publicCode];
if (publicCode && publicConnectionErrorLabels[publicCode]) {
return publicConnectionErrorLabels[publicCode];
}
@@ -13,13 +13,14 @@ import {
Button,
GlassSurface,
Icon,
IconButton,
Select,
StatusBadge,
TextField,
type StatusTone,
} from "@nodedc/ui-react";
import {K1WifiPasswordField} from "./K1WifiPasswordField";
import type {
BleDevice,
ConnectionVerifyRequest,
@@ -1291,12 +1292,6 @@ export function K1ProvisioningPipeline({
const [selectedDeviceSnapshot, setSelectedDeviceSnapshot] = useState<BleDevice | null>(null);
const [ssid, setSsid] = useState("");
const [password, setPassword] = useState("");
const [passwordVisible, setPasswordVisible] = useState(false);
useEffect(() => {
if (password.length === 0 && passwordVisible) {
setPasswordVisible(false);
}
}, [password, passwordVisible]);
const connectionMode = desiredMode;
const [successfulLocalConnect, setSuccessfulLocalConnect] =
useState<CompletedLocalNetworkIntent | null>(null);
@@ -2729,7 +2724,6 @@ export function K1ProvisioningPipeline({
setExplicitProvisioningDraft(null);
setSsid("");
setPassword("");
setPasswordVisible(false);
setConnectionAttemptPresentation(null);
setCandidateUnavailableMessage(null);
setReadOnlyReconnectPresentation(null);
@@ -3107,31 +3101,13 @@ export function K1ProvisioningPipeline({
spellCheck={false}
placeholder={modeCopy.ssidPlaceholder}
/>
<div className="password-field-row">
<TextField
label="Пароль Wi‑Fi"
hint={connectionAttemptOwnsDraft
? "Удалён после отправки"
: "Только в оперативной памяти"}
type={passwordVisible ? "text" : "password"}
value={connectionAttemptOwnsDraft ? "" : password}
onChange={(event) => setPassword(event.target.value)}
disabled={provisioningFieldsDisabled}
autoComplete="off"
placeholder={connectionAttemptOwnsDraft
? "Пароль передан"
: "Введите пароль"}
/>
<IconButton
label={passwordVisible ? "Скрыть пароль" : "Показать пароль"}
shape="rounded"
disabled={provisioningFieldsDisabled || password.length === 0}
aria-pressed={passwordVisible}
onClick={() => setPasswordVisible((visible) => !visible)}
>
<Icon name={passwordVisible ? "eye-off" : "eye"} />
</IconButton>
</div>
<K1WifiPasswordField
value={connectionAttemptOwnsDraft ? "" : password}
onChange={setPassword}
disabled={provisioningFieldsDisabled}
hint={connectionAttemptOwnsDraft ? "Удалён после отправки" : "Только в оперативной памяти"}
placeholder={connectionAttemptOwnsDraft ? "Пароль передан" : "Введите пароль"}
/>
{applyAction}
</div>
)}
@@ -0,0 +1,7 @@
.password-field-row {
display: grid;
min-width: 0;
grid-template-columns: minmax(0, 1fr) auto;
align-items: end;
gap: 0.55rem;
}
@@ -0,0 +1,21 @@
import {useEffect, useState} from 'react';
import {Icon, IconButton, TextField} from '@nodedc/ui-react';
import './K1WifiPasswordField.css';
/** The same Wi-Fi entry composition for direct and onboard K1 enrollment. */
export function K1WifiPasswordField({value, onChange, disabled, hint, placeholder}: {
value:string; onChange:(value:string)=>void; disabled?:boolean;
hint?:string; placeholder?:string;
}) {
const [visible,setVisible]=useState(false);
useEffect(()=>{if(!value)setVisible(false);},[value]);
return <div className="password-field-row">
<TextField label="Пароль Wi‑Fi" type={visible?'text':'password'} value={value}
onChange={event=>onChange(event.target.value)} disabled={disabled}
hint={hint} placeholder={placeholder} autoComplete="off" spellCheck={false}/>
<IconButton label={visible?'Скрыть пароль':'Показать пароль'} shape="rounded"
disabled={disabled||!value} aria-pressed={visible} onClick={()=>setVisible(current=>!current)}>
<Icon name={visible?'eye-off':'eye'}/>
</IconButton>
</div>;
}