From ba0c948ca1846ee9647ba3b13684414fa0869799 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 6 Sep 2026 01:05:19 +0300 Subject: [PATCH] fix(node): show camera connectivity and remove company header marks --- apps/control-station/src/App.tsx | 4 ---- .../test/sensorStatus.test.mjs | 24 +++++++++++++++++++ apps/node-agent/packaging/build_deb.py | 2 +- apps/node-agent/ui/src/main.tsx | 4 ++-- .../node/05_SENSOR_HOST_AND_SHARED_CONTROL.md | 21 ++++++++++++++++ packages/sensor-ui/src/SensorWorkspace.tsx | 5 ++-- packages/sensor-ui/src/sensorStatus.ts | 13 ++++++++++ 7 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 apps/control-station/test/sensorStatus.test.mjs create mode 100644 packages/sensor-ui/src/sensorStatus.ts diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index c5e817c..7bdfebe 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -10,7 +10,6 @@ import { ControlRow, HeaderNavigation, HeaderProfile, - HeaderWorkspace, Icon, Inspector, RangeControl, @@ -648,8 +647,6 @@ export default function App() { brandHref="/" brandLabel="NODEDC MISSION CORE" center={ - <> - - } right={ diff --git a/apps/control-station/test/sensorStatus.test.mjs b/apps/control-station/test/sensorStatus.test.mjs new file mode 100644 index 0000000..b21b4d6 --- /dev/null +++ b/apps/control-station/test/sensorStatus.test.mjs @@ -0,0 +1,24 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import ts from 'typescript'; + +const source=readFileSync(new URL('../../../packages/sensor-ui/src/sensorStatus.ts',import.meta.url),'utf8'); +const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.ESNext}}).outputText; +const {sensorStatus}=await import('data:text/javascript;base64,'+Buffer.from(code).toString('base64')); +const connected={online:true,configured:true,prepared:true,verified:false,snapshot:{acquisition:'idle',enrollment:'enrolled'}}; + +test('a configured, reconnected camera is connected before another frame verification',()=>{ + assert.deepEqual(sensorStatus(connected,true),{label:'Подключено',tone:'success'}); +}); +test('stale board data cannot assert camera connectivity',()=>{ + assert.equal(sensorStatus(connected,false).tone,'neutral'); + assert.equal(sensorStatus({...connected,online:false},true).label,'Не подключено'); +}); +test('a capture failure or unavailable driver is not shown as healthy',()=>{ + assert.equal(sensorStatus({...connected,snapshot:{acquisition:'failed'}},true).tone,'danger'); + assert.equal(sensorStatus({...connected,prepared:false},true).tone,'warning'); +}); +test('an unconfigured camera still requires preparation',()=>{ + assert.deepEqual(sensorStatus({...connected,configured:false},true),{label:'Требуется подготовка',tone:'neutral'}); +}); diff --git a/apps/node-agent/packaging/build_deb.py b/apps/node-agent/packaging/build_deb.py index 66785bb..197acf2 100644 --- a/apps/node-agent/packaging/build_deb.py +++ b/apps/node-agent/packaging/build_deb.py @@ -13,7 +13,7 @@ import tarfile ROOT = Path(__file__).resolve().parents[1] -VERSION = "0.6.9" +VERSION = "0.6.10" BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af" diff --git a/apps/node-agent/ui/src/main.tsx b/apps/node-agent/ui/src/main.tsx index 659d823..64a2224 100644 --- a/apps/node-agent/ui/src/main.tsx +++ b/apps/node-agent/ui/src/main.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; -import { ActivityIndicator, AdminNavigationPanel, AppHeader, ApplicationPanel, ApplicationShell, Button, HeaderNavigation, HeaderProfile, HeaderWorkspace, Icon, SettingsCard, ToastStack, UserProfileMenu, useApplicationWorkspace } from "@nodedc/ui-react"; +import { ActivityIndicator, AdminNavigationPanel, AppHeader, ApplicationPanel, ApplicationShell, Button, HeaderNavigation, HeaderProfile, Icon, SettingsCard, ToastStack, UserProfileMenu, useApplicationWorkspace } from "@nodedc/ui-react"; import "@nodedc/tokens/tokens.css"; import "@nodedc/tokens/themes.css"; import "@nodedc/ui-core/styles.css"; @@ -42,7 +42,7 @@ function App() { : workspace.activeView === "ssh" ? setAdding(false)} /> : null; return <> } brandLabel="Mission Core Node" - center={<> ({ value: item.id, label: item.label, disabled: !value || !item.first || environment.running }))} onChange={selectRoot} />} + center={ ({ value: item.id, label: item.label, disabled: !value || !item.first || environment.running }))} onChange={selectRoot} />} right={ { const next = theme === "dark" ? "light" : "dark"; setTheme(next); localStorage.setItem("node-theme", next); } }]} />} />} navigationOpen={!!value && workspace.navigationOpen} contentOpen={!!value && workspace.contentOpen} contentExpanded={workspace.contentExpanded} navigation={void}){ const [inventory,setInventory]=useState(null);const [selected,setSelected]=useState(null);const [editing,setEditing]=useState(null);const [name,setName]=useState('');const [localBusy,setBusy]=useState(null);const [error,setError]=useState('');const [fresh,setFresh]=useState(false); @@ -27,8 +28,8 @@ export function SensorWorkspace({transport,enabled=true,onDetailChange}:{transpo const operation=inventory.operations?.find(v=>v.device_id===item.id&&v.state==='running');const busy=!!operation||localBusy===item.id; const configured=item.configured??item.snapshot.enrollment==='enrolled'; const prep=operation?.action_id==='prepare'&&inventory.preparation&&(inventory.preparation.started_at*1000>=Date.parse(operation.requested_at)-1000)?inventory.preparation:undefined; - const label=busy?'Подготовка или команда выполняется':!item.online?'Не подключено':item.snapshot.acquisition==='streaming'?item.recording?'Идёт запись':item.playback_id?'Просмотр записи':'Идёт захват':item.verified?'Проверено':configured?'Настроено, ожидает проверки камеры':'Требуется подготовка'; - return
  • } title={item.name} description={item.model} metadata={USB {item.usb}} aria-busy={busy} progress={busy?{label, value:prep?.state==='running'?prep.steps.filter(s=>s.state==='complete').length/(prep.steps.length+1):prep?.state==='complete'?5/6:undefined,valueText:prep?.steps.find(s=>s.state==='running')?.label??'Проверка кадров камеры'}:undefined} status={{configured&&item.online?null:label}} actions={<>{!configured&&void action(item,'prepare')}>}{setEditing(item);setName(item.name);}}>setSelected(item.id)}>}/>
  • ;})}} + const status=sensorStatus(item,enabled&&fresh);const label=busy?'Подготовка или команда выполняется':status.label; + return
  • } title={item.name} description={item.model} metadata={USB {item.usb}} aria-busy={busy} progress={busy?{label, value:prep?.state==='running'?prep.steps.filter(s=>s.state==='complete').length/(prep.steps.length+1):prep?.state==='complete'?5/6:undefined,valueText:prep?.steps.find(s=>s.state==='running')?.label??'Проверка кадров камеры'}:undefined} status={{configured&&item.online?null:label}} actions={<>{!configured&&void action(item,'prepare')}>}{setEditing(item);setName(item.name);}}>setSelected(item.id)}>}/>
  • ;})}} {(inventory?.operations?.some(v=>v.state==='running'&&v.action_id==='prepare'&&!!inventory.preparation&&inventory.preparation.started_at*1000>=Date.parse(v.requested_at)-1000))&&inventory?.preparation&&{inventory.preparation.steps.map(step=>:step.state==='running'?:{step.state==='error'?'Ошибка':step.state==='blocked'?'Не выполнено':'Ожидает'}}/>) }:Ожидает}/>} } setEditing(null)} footer={}>
    setName(e.target.value)} disabled={!!localBusy}/>{editing&&(editing.configured??editing.snapshot.enrollment==='enrolled')&&{const target=editing;setEditing(null);void action(target,'prepare');}}>Обновить}/>}
    diff --git a/packages/sensor-ui/src/sensorStatus.ts b/packages/sensor-ui/src/sensorStatus.ts new file mode 100644 index 0000000..84b6981 --- /dev/null +++ b/packages/sensor-ui/src/sensorStatus.ts @@ -0,0 +1,13 @@ +import type {Sensor} from './contracts'; + +// The row lamp describes current connectivity, not a past frame-verification run. +export function sensorStatus(sensor:Sensor, fresh:boolean):{label:string;tone:'neutral'|'success'|'warning'|'danger'} { + if(!fresh)return {label:'Нет свежих сведений с БК',tone:'neutral'}; + if(!sensor.online)return {label:'Не подключено',tone:'neutral'}; + if(sensor.snapshot.acquisition==='failed')return {label:'Ошибка захвата',tone:'danger'}; + const configured=sensor.configured??sensor.snapshot.enrollment==='enrolled'; + if(!configured)return {label:'Требуется подготовка',tone:'neutral'}; + if(!sensor.prepared)return {label:'Драйвер недоступен',tone:'warning'}; + if(sensor.snapshot.acquisition==='streaming')return {label:sensor.recording?'Идёт запись':sensor.playback_id?'Просмотр записи':'Идёт захват',tone:'success'}; + return {label:'Подключено',tone:'success'}; +}