diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 27d35fb..4900db8 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -842,7 +842,9 @@ export default function App() { laboratoryAnnotation.control ) : activeDefinition.kind === "observatory" ? ( Только наблюдение - ) : activeDefinition.kind === "contour-health" ? null : activeDefinition.root === "system" ? ( + ) : activeDefinition.kind === "contour-health" ? ( +
+ ) : activeDefinition.root === "system" ? ( workerAvailable(item.telemetry)).length+(snapshot.vehicles??[]).filter(boardAvailable).length; + const online=Number(snapshot.core!==null)+(snapshot.workers??[]).filter(item=>workerAvailable(item.telemetry)).length+(snapshot.vehicles??[]).filter(boardAvailable).length; if(snapshot.errors.length)return {tone:'warning' as const,label:'Проверка неполная',online,total}; - if(!snapshot.core?.ok)return {tone:'danger' as const,label:'Mission Core недоступен',online,total}; - return {tone:online===total?'success' as const:'warning' as const,label:online===total?'Все узлы на связи':'Часть узлов не на связи',online,total}; + if(!snapshot.core)return {tone:'danger' as const,label:'Mission Core не в сети',online,total}; + if(!snapshot.core.ok)return {tone:'warning' as const,label:'Mission Core работает с ограничениями',online,total}; + if(snapshot.workers?.some(item=>workerConnectionStatus(item.telemetry).state==='unknown'))return {tone:'warning' as const,label:'Состояние части узлов не подтверждено',online,total}; + return {tone:online===total?'success' as const:'warning' as const,label:online===total?'Все узлы в сети':'Часть узлов не в сети',online,total}; } diff --git a/apps/control-station/src/core/system/workerConnectionStatus.ts b/apps/control-station/src/core/system/workerConnectionStatus.ts new file mode 100644 index 0000000..1da67b8 --- /dev/null +++ b/apps/control-station/src/core/system/workerConnectionStatus.ts @@ -0,0 +1,13 @@ +import type {WorkerTelemetry} from './workerTelemetry'; + +// Receiver failure is not evidence that the remote host has shut down. +export function workerConnectionStatus(telemetry: WorkerTelemetry | null) { + const connection = telemetry?.connection; + if (!connection) return {state:'unknown', tone:'warning', label:'Не в сети', detail:'Состояние вычислителя пока не подтверждено.'} as const; + if (connection.reachable && !connection.identity_matches) return {state:'unknown', tone:'warning', label:'Не в сети', detail:'Ответивший узел не совпадает с выбранным вычислителем.'} as const; + if (connection.reachable && connection.identity_matches && telemetry.node) return {state:'online', tone:'success', label:'В сети', detail:null} as const; + if (['telemetry-receiver-unavailable','telemetry-agent-unavailable','invalid-telemetry-response'].includes(connection.error_code ?? '')) { + return {state:'unknown', tone:'warning', label:'Не в сети', detail:'Mission Core не получает данные через службу телеметрии. Состояние самого вычислителя не подтверждено.'} as const; + } + return {state:'unknown', tone:'warning', label:'Не в сети', detail:'Последние данные не подтверждают текущее состояние вычислителя.'} as const; +} diff --git a/apps/control-station/src/workspaces/ContourHealthWorkspace.tsx b/apps/control-station/src/workspaces/ContourHealthWorkspace.tsx index c3abffe..4b58e51 100644 --- a/apps/control-station/src/workspaces/ContourHealthWorkspace.tsx +++ b/apps/control-station/src/workspaces/ContourHealthWorkspace.tsx @@ -1,7 +1,9 @@ import {useEffect,useState} from 'react'; -import {Button,Icon,StatusBadge} from '@nodedc/ui-react'; +import {createPortal} from 'react-dom'; +import {workerConnectionStatus} from '../core/system/workerConnectionStatus'; +import {IconButton,Icon,StatusBadge} from '@nodedc/ui-react'; import type {MissionRuntimeState} from '../core/runtime/contracts'; -import {boardAvailable,contourSummary,readContourSnapshot,workerAvailable,type ContourSnapshot} from '../core/system/contourHealth'; +import {boardAvailable,contourSummary,readContourSnapshot,type ContourSnapshot} from '../core/system/contourHealth'; import {startSequentialPolling} from '../core/system/sequentialPolling'; export function contourRuntimeAuthorityPresentation( @@ -28,7 +30,7 @@ function observed(value:number|string|null|undefined){ const date=new Date(value); return Number.isFinite(date.getTime())?date.toLocaleString('ru-RU'):'—'; } -export function ContourHealthWorkspace({state}:{state:MissionRuntimeState|null}){ +export function ContourHealthWorkspace({state,headerToolsHost}:{state:MissionRuntimeState|null;headerToolsHost?:HTMLElement|null}){ const [snapshot,setSnapshot]=useState(null); const [loading,setLoading]=useState(true),[generation,setGeneration]=useState(0); useEffect(()=>{ @@ -43,33 +45,36 @@ export function ContourHealthWorkspace({state}:{state:MissionRuntimeState|null}) const summary=contourSummary(snapshot); const core=snapshot?.core; const {controlledDevice,deviceControlConnectivity}=contourRuntimeAuthorityPresentation(state); + const refresh=setGeneration(value=>value+1)}>; return
+ {headerToolsHost?createPortal(refresh,headerToolsHost):refresh}

Узлы и связь

Mission Core, зарегистрированные вычислители и бортовые компьютеры аппаратов.

-
{loading?'Проверяем состояние':summary.label}
+
{loading?'Проверяем состояние':summary.label}
-
Узлы на связи{summary.online??'—'} / {summary.total??'—'}Core, вычислители и бортовые ПК
+
Узлы в сети{summary.online??'—'} / {summary.total??'—'}Core, вычислители и бортовые ПК
Вычислители{snapshot?.workers?.length??'—'}из настроек системы
Бортовые компьютеры{snapshot?.vehicles?.length??'—'}из реестра аппаратов
Последняя проверка{observed(snapshot?.observedAt)}обновление каждые 5 секунд
-
-
КОМПЬЮТЕР ОПЕРАТОРА

Mission Core

{core?(core.ok?'Доступен':'Недоступен'):snapshot?'Нет свежих данных':'Проверяем'}
+
+
КОМПЬЮТЕР ОПЕРАТОРА

Mission Core

{core?'В сети':snapshot?'Не в сети':'Проверяем состояние'}
Версия
{core?.version??'—'}
Время ответа
{snapshot?.latency==null?'—':`${Math.max(1,Math.round(snapshot.latency))} мс`}
Обработчики устройств готовы
{core?`${core.plugin_runtimes.ready} / ${core.plugin_runtimes.total}`:'—'}
+ {!core?.ok&&core?.components?.recording_cache?.status==='capacity-pressure'&&Недостаточно свободного места для новых записей.}
{snapshot?.workers?.map(({contour,telemetry})=>{ - const online=workerAvailable(telemetry); - const mismatch=telemetry?.connection.reachable&&!telemetry.connection.identity_matches; - return
-
ВЫЧИСЛИТЕЛЬ

{contour.display_name}

{online?'На связи':mismatch?'Другой узел':telemetry?'Нет свежих данных':'Проверка недоступна'}
+ const status=workerConnectionStatus(telemetry); + return
+
ВЫЧИСЛИТЕЛЬ

{contour.display_name}

{status.label}
Узел
{contour.expected_node_id}
Телеметрия
{contour.telemetry_mode==='agent-mqtt'?'Агент':'Диагностика SSH'}
Проверено
{observed(telemetry?.connection.observed_at_utc)}
Последнее измерение
{observed(telemetry?.node?.observed_at_utc)}
Операционная система
{telemetry?.node?.os.caption??'—'}
+ {status.detail&&{status.detail}}
; })} {snapshot?.vehicles?.map(vehicle=>{ const online=boardAvailable(vehicle); - const status=vehicle.enrollment==='revoked'?'Привязка отозвана':vehicle.enrollment==='pending'?'Подключение':vehicle.enrollment==='failed'?'Не подключён':online?'На связи':'Нет связи'; + const status=vehicle.enrollment==='revoked'?'Привязка отозвана':vehicle.enrollment==='pending'?'Подключение':vehicle.enrollment==='failed'?'Не подключён':online?'В сети':'Не в сети'; return
БОРТОВОЙ КОМПЬЮТЕР

{vehicle.name}

{status}
Имя БК
{vehicle.host?.hostname??'—'}
Операционная система
{vehicle.host?.os??'—'}
Последняя связь
{observed(vehicle.last_seen===null?null:vehicle.last_seen*1000)}
diff --git a/apps/control-station/src/workspaces/Workspaces.tsx b/apps/control-station/src/workspaces/Workspaces.tsx index 7c7d476..7b0fb83 100644 --- a/apps/control-station/src/workspaces/Workspaces.tsx +++ b/apps/control-station/src/workspaces/Workspaces.tsx @@ -437,6 +437,7 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) { case "contour-health": return ( ); diff --git a/apps/control-station/src/workspaces/fleet/BoardComputer.tsx b/apps/control-station/src/workspaces/fleet/BoardComputer.tsx index 2e0f544..bba33c7 100644 --- a/apps/control-station/src/workspaces/fleet/BoardComputer.tsx +++ b/apps/control-station/src/workspaces/fleet/BoardComputer.tsx @@ -1,7 +1,7 @@ import {Button,Icon,IconButton,ResourceRow,StatusBadge,Window} from '@nodedc/ui-react'; import type {Vehicle} from '../../core/fleet/useFleet'; -export const boardStatus=(item:Vehicle,available=true)=>item.enrollment==='pending'?'Подтверждаем привязку':item.enrollment==='revoked'?'Не привязан':item.enrollment==='failed'?'Привязка не завершена':item.connectivity==='online'?(available?'В сети':'Нет свежих данных'):'Нет связи'; +export const boardStatus=(item:Vehicle,available=true)=>item.enrollment==='pending'?'Подтверждаем привязку':item.enrollment==='revoked'?'Не привязан':item.enrollment==='failed'?'Привязка не завершена':item.connectivity==='online'?(available?'В сети':'Не в сети'):'Не в сети'; export function BoardComputerRow({vehicle,enabled,showingSavedDevices,onSettings,onMonitor,onAdd,open,onToggle}:{vehicle:Vehicle;enabled:boolean;showingSavedDevices:boolean;onSettings:()=>void;onMonitor:()=>void;onAdd?:()=>void;open:boolean;onToggle:()=>void}){ const label=boardStatus(vehicle,enabled); return } title="Бортовой компьютер" description={vehicle.host?.hostname||'Mission Core Node'} metadata={`${label}${showingSavedDevices?' (показаны последние сохранённые устройства)':''}`} statusPlacement="leading" diff --git a/apps/control-station/src/workspaces/system/ComputeModulesWorkspace.tsx b/apps/control-station/src/workspaces/system/ComputeModulesWorkspace.tsx index 58c83ca..572b628 100644 --- a/apps/control-station/src/workspaces/system/ComputeModulesWorkspace.tsx +++ b/apps/control-station/src/workspaces/system/ComputeModulesWorkspace.tsx @@ -1,3 +1,4 @@ +import {workerConnectionStatus} from '../../core/system/workerConnectionStatus'; import { GlassSurface, StatusBadge, @@ -77,9 +78,7 @@ export function ComputeModulesWorkspace() { const node = telemetry?.node ?? null; const missionCoreRuntimes = telemetry?.runtimes.filter((runtime) => !runtime.external) ?? []; const externalRuntimes = telemetry?.runtimes.filter((runtime) => runtime.external) ?? []; - const connected = Boolean( - telemetry?.connection.reachable && telemetry.connection.identity_matches && node, - ); + const connectionStatus = workerConnectionStatus(telemetry); const agentTelemetry = telemetry?.connection.source === "agent-mqtt"; const history = telemetry?.history ?? []; const cpuPercent = node?.cpu.load_percent; @@ -105,14 +104,13 @@ export function ComputeModulesWorkspace() {

- - {connected - ? agentTelemetry ? "Агент доступен" : "SSH-диагностика" - : "Нет свежих данных"} + + {connectionStatus.label}
+ {connectionStatus.detail &&

{connectionStatus.detail}

} {error ? ( {error} diff --git a/apps/control-station/src/workspaces/system/NetworkWorkspace.tsx b/apps/control-station/src/workspaces/system/NetworkWorkspace.tsx index 4a42d22..f557971 100644 --- a/apps/control-station/src/workspaces/system/NetworkWorkspace.tsx +++ b/apps/control-station/src/workspaces/system/NetworkWorkspace.tsx @@ -1,3 +1,4 @@ +import {workerConnectionStatus} from '../../core/system/workerConnectionStatus'; import { GlassSurface, StatusBadge, @@ -31,6 +32,7 @@ export function NetworkWorkspace() { const connected = Boolean( telemetry?.connection.reachable && telemetry.connection.identity_matches, ); + const connectionStatus = workerConnectionStatus(telemetry); const agentTelemetry = telemetry?.connection.source === "agent-mqtt"; const runtimeOnline = telemetry?.runtimes.some( (runtime) => !runtime.external && runtime.state === "running", @@ -43,17 +45,18 @@ export function NetworkWorkspace() { СИСТЕМА / СЕТЬ

{selectedContour?.display_name ?? "Контур не выбран"}

- Фактический локальный маршрут и сетевые счётчики выбранного вычислительного + Фактический маршрут и сетевые счётчики выбранного вычислительного контура. Адрес, транспорт и установка агента находятся в настройках контура.

- - {connected ? "Маршрут доступен" : "Нет свежих данных"} + + {connectionStatus.label}
+ {connectionStatus.detail &&

{connectionStatus.detail}

} {error ? ( {error} @@ -109,7 +112,7 @@ export function NetworkWorkspace() { 127.0.0.1:8000