From d7986cb62f2fad01f4a37faab10d971eaf29f266 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 7 Sep 2026 13:27:21 +0300 Subject: [PATCH] Group onboard wireless steps and trace journalled Bluetooth failures --- .../test/sensorEnrollment.test.mjs | 30 ++++++++- apps/node-agent/packaging/build_deb.py | 2 +- docs/audits/2026-09-07-k1-wireless-flow-r5.md | 10 +-- .../audits/2026-09-07-k1-wireless-steps-r6.md | 66 +++++++++++++++++++ .../src/sensors/DeviceEnrollmentWindow.tsx | 65 ++++++++++++------ .../frontend/src/sensors/enrollment.ts | 8 +++ .../frontend/src/sensors/revealEnrollment.ts | 19 ++++++ plugins/xgrids-k1/packaging/build_deb.py | 2 +- .../device_plugins/xgrids_k1/node_bridge.py | 15 ++++- tests/test_node_k1_bridge.py | 27 ++++++++ 10 files changed, 217 insertions(+), 27 deletions(-) create mode 100644 docs/audits/2026-09-07-k1-wireless-steps-r6.md create mode 100644 plugins/xgrids-k1/frontend/src/sensors/revealEnrollment.ts diff --git a/apps/control-station/test/sensorEnrollment.test.mjs b/apps/control-station/test/sensorEnrollment.test.mjs index be4a659..94582ae 100644 --- a/apps/control-station/test/sensorEnrollment.test.mjs +++ b/apps/control-station/test/sensorEnrollment.test.mjs @@ -2,10 +2,11 @@ import assert from 'node:assert/strict'; import {before,after,test} from 'node:test'; import {readFileSync,readdirSync} from 'node:fs'; import {createServer} from 'vite'; -let server,api,resolveContribution,wirelessContributions; +let server,api,resolveContribution,wirelessContributions,revealEnrollment; before(async()=>{ server=await createServer({appType:'custom',logLevel:'silent',server:{middlewareMode:true}}); api=await server.ssrLoadModule('@xgrids-k1/frontend/sensors/enrollment.ts'); + ({revealEnrollment}=await server.ssrLoadModule('@xgrids-k1/frontend/sensors/revealEnrollment.ts')); ({sensorContribution:resolveContribution}=await server.ssrLoadModule('../../packages/sensor-ui/src/extensions.ts')); ({wirelessContributions}=await server.ssrLoadModule('../../packages/sensor-ui/src/extensions.ts')); }); @@ -78,6 +79,33 @@ test('station refusal is the same message in onboard and LAB presentations',()=> assert.match(api.enrollmentNotice(state),/Проверьте название сети и пароль/); assert.doesNotMatch(api.enrollmentNotice(state),/Bluetooth.*ошиб/); }); +test('Bluetooth failure before dispatch asks for discovery without blaming Wi-Fi',()=>{ + const state={...initial,connection_attempt:{schema_version:'missioncore.xgrids-k1-connection-attempt/v1',attempt_id:'test',status:'failed',stage:'connect-failed',phase:'network_not_applied',public_error_code:'BleakError',side_effect_status:'none'}}; + assert.equal(api.enrollmentBluetoothFailure(state),true); + assert.match(api.enrollmentNotice(state),/Bluetooth.*не были отправлены/s); + assert.doesNotMatch(api.enrollmentNotice(state),/Проверьте.*пароль/); + const unknown={...state,connection_attempt:{...state.connection_attempt,phase:'network_outcome_unknown',side_effect_status:'unknown'}}; + assert.equal(api.enrollmentBluetoothFailure(unknown),false); + assert.doesNotMatch(api.enrollmentNotice(unknown),/не были отправлены/); +}); +test('new workflow content scrolls only the containing viewport without stealing focus',()=>{ + const original={document:globalThis.document,window:globalThis.window,getComputedStyle:globalThis.getComputedStyle}; + const calls=[];let reduced=false; + const body={}; + const scroller={parentElement:body,scrollHeight:1600,clientHeight:500,scrollTop:100, + getBoundingClientRect:()=>({top:100,bottom:600}),scrollTo:value=>calls.push(value)}; + globalThis.document={body,documentElement:{}}; + globalThis.window={matchMedia:()=>({matches:reduced})}; + globalThis.getComputedStyle=element=>({overflowY:element===scroller?'auto':'visible'}); + const target=(top,height)=>({parentElement:scroller,getBoundingClientRect:()=>({top,bottom:top+height,height}),focus:()=>assert.fail('focus must remain with the operator')}); + try{ + revealEnrollment(target(120,250));assert.equal(calls.length,0); + revealEnrollment(target(620,100));assert.deepEqual(calls.pop(),{top:220,behavior:'smooth'}); + reduced=true; + revealEnrollment(target(620,800));assert.deepEqual(calls.pop(),{top:620,behavior:'instant'}); + revealEnrollment({...target(700,100),parentElement:body});assert.equal(calls.length,0); + }finally{Object.assign(globalThis,original);} +}); test('sensor host can resolve zero or unrelated integrations and rejects ambiguity',()=>{ const alpha={kind:'alpha',Detail:()=>null},beta={kind:'beta',Detail:()=>null}; assert.equal(resolveContribution([],{kind:'alpha'}),undefined); diff --git a/apps/node-agent/packaging/build_deb.py b/apps/node-agent/packaging/build_deb.py index 270d0e7..2806c6d 100644 --- a/apps/node-agent/packaging/build_deb.py +++ b/apps/node-agent/packaging/build_deb.py @@ -11,7 +11,7 @@ import sys ROOT = Path(__file__).resolve().parents[1] -VERSION = "0.8.3" +VERSION = "0.8.4" sys.path.insert(0, str(ROOT.parents[1] / "scripts/packaging")) from debian import package diff --git a/docs/audits/2026-09-07-k1-wireless-flow-r5.md b/docs/audits/2026-09-07-k1-wireless-flow-r5.md index 69147ff..e677ae7 100644 --- a/docs/audits/2026-09-07-k1-wireless-flow-r5.md +++ b/docs/audits/2026-09-07-k1-wireless-flow-r5.md @@ -79,9 +79,11 @@ The release is retained in `private/releases/mission-core-node-20260907-r5`. Owner screenshots, redacted UTC/monotonic manifest and validation logs are hashed under `private/acceptance/k1-node083-20260907-core-ui`. -One Ubuntu GUI installer is open and awaiting owner sudo authentication. -The latest observed installed versions are still Node 0.8.2 and K1 -0.1.2+private.1, both active. Installation and new UI hardware acceptance must -not be inferred from the package build. Git publication remains pending the +The owner completed Ubuntu authentication; the installer exited with code 0. +Readback confirmed Node 0.8.3 and K1 0.1.2+private.1, both active with zero +restarts, started at 13:08:15 MSK. This confirms the Node-only upgrade restored +the existing plugin. The next owner UI attempt found K1 but failed at Bluetooth +connect before Wi-Fi dispatch; see the R6 report. Fresh-cache Bridge/live +acceptance remains open. Git publication remains pending the explicit owner reply to the prior automatic approval rejection of deployment reports; no push workaround was used. diff --git a/docs/audits/2026-09-07-k1-wireless-steps-r6.md b/docs/audits/2026-09-07-k1-wireless-steps-r6.md new file mode 100644 index 0000000..154bf0e --- /dev/null +++ b/docs/audits/2026-09-07-k1-wireless-steps-r6.md @@ -0,0 +1,66 @@ +# Wireless workflow groups and failure observability R6 + +The owner requested separate Bluetooth and Wi-Fi steps and automatic reveal of +new content in the bounded wireless-enrollment modal. This is a composition of +existing SettingsCard, ResourceList, ResourceRow, Button and field primitives, +with no new Design Guideline entity, surface decoration or navigation change. +Both Core and Node consume the same plugin-owned form. + +Bluetooth discovery and selectable candidates belong to step 01. Step 02 appears +after explicit selection and groups the K1 Wi-Fi network, credentials, connection +check and its result. The check description is fully visible in the card header; +the action is below the fields. The final Connect action still requires the +current successful proof and sends no second physical command. + +Explicit discovery results, selected-device Wi-Fi, network choices and changed +connection results reveal themselves inside the existing nearest scrollable +surface. Poll revisions and typing do not trigger scrolling. Fully visible +content does not move, tall steps reveal their heading, reduced-motion uses an +instant move, and keyboard focus stays with the operator. The page behind the +modal is never scrolled. A completed rescan dismisses the preceding attempt's +message without suppressing a later outage or new result. + +## Actual failed attempt + +The owner UI attempt at 10:09:50.958 UTC completed at 10:09:51.021 UTC with +BleakError, stage connect-failed, phase network_not_applied and side-effect +status none. Wi-Fi settings were not sent. Installed BlueZ is 5.72; both Node +0.8.3 and optional K1 0.1.2 services were active without restarts. This is not +evidence of a wrong network name/password or an Ethernet routing fault. + +The exact Bluetooth exception origin is not recoverable from this deployment's +public projection or system journal. The facade records structured fields, but +the default service logger prints only the fixed message; Node's outer HTTP +exception handler never sees journalled failures returned as normal results. +R6 logs a validated operation ID, internal action, exception class and source +file/function/line locations at that existing catch boundary. It never logs +exception text, source lines, local values, credentials or payloads. The +invocation still runs once and then reads only its exact journal result. + +An explicit pre-dispatch Bluetooth connection failure is now shown in step 01, +says Wi-Fi settings were not sent and requests a fresh scan. Its failed native +selection cannot be reused in the dialog. Unknown write outcomes retain their +existing read-only reconciliation path and are never described as unsent. +The next clean-cache owner UI attempt is needed to identify the precise +Bluetooth backend failure; this increment does not claim that physical +connection failure is fixed. + +## Scope and validation + +No BLE framing, mutation/retry policy, runtime fences, MQTT supervisor, host +network switching, LAB connection flow or Rerun profile changes. The board may +remain on Ethernet while K1 joins the router's Wi-Fi. The optional backend change +is confined to NodeBridge diagnostics; the local LAB facade is unchanged. + +Focused architecture/enrollment checks: 22 passed. Full Core frontend suite: +793 passed. Focused NodeBridge and real package-maintainer lifecycle tests: +16 passed, including secret-free failure logging without a second invocation. +Core TypeScript and production build passed. Changed backend files pass Ruff; +the Node package builder has 17 pre-existing lint violations outside the +version-only edit. Repository-wide lint success is not claimed. + +Node 0.8.4 and optional K1 0.1.3 are reserved for this increment. Exact build, +installation and owner UI acceptance must be recorded separately. Private +screenshots, observations, UTC/monotonic manifest and hashes are retained under +private/acceptance/k1-node084-20260907-core-ui. Cache clearance is not yet +explicitly confirmed for the preceding owner observation. diff --git a/plugins/xgrids-k1/frontend/src/sensors/DeviceEnrollmentWindow.tsx b/plugins/xgrids-k1/frontend/src/sensors/DeviceEnrollmentWindow.tsx index 4f70338..2dda3d6 100644 --- a/plugins/xgrids-k1/frontend/src/sensors/DeviceEnrollmentWindow.tsx +++ b/plugins/xgrids-k1/frontend/src/sensors/DeviceEnrollmentWindow.tsx @@ -1,7 +1,8 @@ import {useEffect,useRef,useState} from 'react'; import {ActivityIndicator,Button,ResourceList,ResourceRow,Select,SettingsCard,StatusBadge,TextField,ToastStack} from '@nodedc/ui-react'; import type {SensorEnrollmentProps} from '@mission-core/sensor-sdk'; -import {bridgeFormValid,enroll,enrollmentAllowed,connectionAttempt,enrollmentNotice,mergeEnrollmentState,enrollmentProof,enrollmentProofCurrent,type EnrollmentProof,type EnrollmentState} from './enrollment'; +import {bridgeFormValid,enroll,enrollmentAllowed,connectionAttempt,enrollmentNotice,enrollmentBluetoothFailure,mergeEnrollmentState,enrollmentProof,enrollmentProofCurrent,type EnrollmentProof,type EnrollmentState} from './enrollment'; +import {revealEnrollment} from './revealEnrollment'; export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow}:SensorEnrollmentProps){ const [state,setState]=useState(null); @@ -15,6 +16,9 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow} const [scanned,setScanned]=useState(false); const [verified,setVerified]=useState(null); const [needsScan,setNeedsScan]=useState(false); + const [dismissedAttempt,setDismissedAttempt]=useState(''); + const [reveal,setReveal]=useState({target:'',revision:0}); + const content=useRef(null); const lifetime=useRef(null); const running=useRef(false); @@ -41,7 +45,8 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow} const attempt=connectionAttempt(state); const waiting=attempt?.status==='accepted'||attempt?.status==='running'; - const notice=state?enrollmentNotice(state):''; + const notice=state&&(!state.available||state.fresh===false||attempt?.attempt_id!==dismissedAttempt)?enrollmentNotice(state):''; + const bluetoothFailure=!!notice&&enrollmentBluetoothFailure(state); useEffect(()=>{ setDevice('');setPassword('');setSSID('');setNetwork('manual');setNetworks([]); setVerified(null); @@ -52,6 +57,17 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow} const selected=state?.candidates?.some(value=>value.id===device)??false; const pending=!!busy||waiting; const confirmed=enrollmentProofCurrent(verified,state,device); + function show(target:string){setReveal(current=>({target,revision:current.revision+1}));} + useEffect(()=>{ + if(notice&&(scanned||device))show('result'); + },[notice,confirmed]); + useEffect(()=>{ + const frame=requestAnimationFrame(()=>{ + const target=content.current?.querySelector(`[data-enrollment-reveal="${reveal.target}"]`); + if(target)revealEnrollment(target); + }); + return()=>cancelAnimationFrame(frame); + },[reveal]); async function run(action:'scan'|'networks'|'connect'|'verify'){ if(!state||busy||running.current)return; @@ -72,10 +88,13 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow} if(signal?.aborted)return; onChange(); setState(current=>mergeEnrollmentState(current,{...result,node_id:state.node_id,name:state.name})); - if(action==='scan'){setDevice('');setScanned(true);} - if(action==='networks')setNetworks(result.networks??[]); - if(action==='connect'||action==='verify')setVerified(enrollmentProof(result,device)); - if(action==='connect'&&connectionAttempt(result)?.public_error_code==='network-provision-candidate-not-fresh'){ + if(action==='scan'){ + setDevice('');setScanned(true);setNeedsScan(false); + setDismissedAttempt(connectionAttempt(result)?.attempt_id??'');show('devices'); + } + if(action==='networks'){setNetworks(result.networks??[]);show('networks');} + if(action==='connect'||action==='verify'){setVerified(enrollmentProof(result,device));setDismissedAttempt('');} + if(action==='connect'&&enrollmentBluetoothFailure(result)){ setNeedsScan(true);setDevice(''); } if(result.command_result?.status==='rejected')setError(enrollmentNotice(result)); @@ -90,7 +109,7 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow} return renderWindow({busy:pending, actions:confirmed?:undefined, - content:<> + content:
: @@ -98,38 +117,46 @@ export function DeviceEnrollmentWindow({transport,onChange,onClose,renderWindow} {busy!=='loading'&&!ready?:ready&&<> - :undefined} - onClick={()=>void run('scan')}>{busy==='scan'?'Ищем K1':'Найти K1'}}/> + onClick={()=>void run('scan')}>{busy==='scan'?'Ищем K1':'Найти K1'}}> +
{scanned&&!state?.candidates?.length&&} {!!state?.candidates?.length&& {state.candidates.map(value=>
  • {setDevice(value.id);setPassword('');setVerified(null);}}>{device===value.id?'Выбрано':'Выбрать'}}/>
  • )} + actions={}/>)}
    } - {selected&&!confirmed&&<> - + {bluetoothFailure&&
    } +
    + {selected&&
    :undefined} - onClick={()=>void run('networks')}>{busy==='networks'?'Ищем сети':'Найти сети'}}/> + onClick={()=>void run('networks')}>{busy==='networks'?'Ищем сети':'Найти сети'}}> + {!confirmed&&<> +
    {!!networks.length&&