fix(ui): unify connectivity labels and move contour refresh to header
This commit is contained in:
@@ -842,7 +842,9 @@ export default function App() {
|
|||||||
laboratoryAnnotation.control
|
laboratoryAnnotation.control
|
||||||
) : activeDefinition.kind === "observatory" ? (
|
) : activeDefinition.kind === "observatory" ? (
|
||||||
<StatusBadge tone="neutral">Только наблюдение</StatusBadge>
|
<StatusBadge tone="neutral">Только наблюдение</StatusBadge>
|
||||||
) : activeDefinition.kind === "contour-health" ? null : activeDefinition.root === "system" ? (
|
) : activeDefinition.kind === "contour-health" ? (
|
||||||
|
<div ref={setWorkspaceHeaderToolsHost} />
|
||||||
|
) : activeDefinition.root === "system" ? (
|
||||||
<SystemWorkspaceSelector
|
<SystemWorkspaceSelector
|
||||||
value={activeDefinition.id}
|
value={activeDefinition.id}
|
||||||
onChange={openNavigationView}
|
onChange={openNavigationView}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import {fetchComputeContours,type ComputeContour} from './computeContours';
|
import {fetchComputeContours,type ComputeContour} from './computeContours';
|
||||||
import {fetchWorkerTelemetry,type WorkerTelemetry} from './workerTelemetry';
|
import {fetchWorkerTelemetry,type WorkerTelemetry} from './workerTelemetry';
|
||||||
import type {Vehicle} from '../fleet/useFleet';
|
import type {Vehicle} from '../fleet/useFleet';
|
||||||
|
import {workerConnectionStatus} from './workerConnectionStatus';
|
||||||
|
|
||||||
export interface CoreHealth {
|
export interface CoreHealth {
|
||||||
ok:boolean; service:string; version:string;
|
ok:boolean; service:string; version:string;
|
||||||
plugin_runtimes:{ready:number;total:number};
|
plugin_runtimes:{ready:number;total:number};
|
||||||
|
components?:{recording_cache?:{status?:string}};
|
||||||
}
|
}
|
||||||
export interface ContourSnapshot {
|
export interface ContourSnapshot {
|
||||||
core:CoreHealth|null; latency:number|null;
|
core:CoreHealth|null; latency:number|null;
|
||||||
@@ -59,8 +61,10 @@ export function boardAvailable(vehicle:Vehicle):boolean{
|
|||||||
export function contourSummary(snapshot:ContourSnapshot|null){
|
export function contourSummary(snapshot:ContourSnapshot|null){
|
||||||
if(!snapshot)return {tone:'warning' as const,label:'Проверяем состояние',online:null,total:null};
|
if(!snapshot)return {tone:'warning' as const,label:'Проверяем состояние',online:null,total:null};
|
||||||
const total=snapshot.workers!==null&&snapshot.vehicles!==null?1+snapshot.workers.length+snapshot.vehicles.length:null;
|
const total=snapshot.workers!==null&&snapshot.vehicles!==null?1+snapshot.workers.length+snapshot.vehicles.length:null;
|
||||||
const online=Number(Boolean(snapshot.core?.ok))+(snapshot.workers??[]).filter(item=>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.errors.length)return {tone:'warning' as const,label:'Проверка неполная',online,total};
|
||||||
if(!snapshot.core?.ok)return {tone:'danger' as const,label:'Mission Core недоступен',online,total};
|
if(!snapshot.core)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.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};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import {useEffect,useState} from 'react';
|
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 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';
|
import {startSequentialPolling} from '../core/system/sequentialPolling';
|
||||||
|
|
||||||
export function contourRuntimeAuthorityPresentation(
|
export function contourRuntimeAuthorityPresentation(
|
||||||
@@ -28,7 +30,7 @@ function observed(value:number|string|null|undefined){
|
|||||||
const date=new Date(value);
|
const date=new Date(value);
|
||||||
return Number.isFinite(date.getTime())?date.toLocaleString('ru-RU'):'—';
|
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<ContourSnapshot|null>(null);
|
const [snapshot,setSnapshot]=useState<ContourSnapshot|null>(null);
|
||||||
const [loading,setLoading]=useState(true),[generation,setGeneration]=useState(0);
|
const [loading,setLoading]=useState(true),[generation,setGeneration]=useState(0);
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
@@ -43,33 +45,36 @@ export function ContourHealthWorkspace({state}:{state:MissionRuntimeState|null})
|
|||||||
const summary=contourSummary(snapshot);
|
const summary=contourSummary(snapshot);
|
||||||
const core=snapshot?.core;
|
const core=snapshot?.core;
|
||||||
const {controlledDevice,deviceControlConnectivity}=contourRuntimeAuthorityPresentation(state);
|
const {controlledDevice,deviceControlConnectivity}=contourRuntimeAuthorityPresentation(state);
|
||||||
|
const refresh=<IconButton label={loading?"Проверяем состояние":"Обновить состояние контура"} disabled={loading} onClick={()=>setGeneration(value=>value+1)}><Icon name="refresh" size={16}/></IconButton>;
|
||||||
return <div className="contour-health-dashboard">
|
return <div className="contour-health-dashboard">
|
||||||
|
{headerToolsHost?createPortal(refresh,headerToolsHost):refresh}
|
||||||
<section className="contour-health-summary">
|
<section className="contour-health-summary">
|
||||||
<div><h2>Узлы и связь</h2><p>Mission Core, зарегистрированные вычислители и бортовые компьютеры аппаратов.</p></div>
|
<div><h2>Узлы и связь</h2><p>Mission Core, зарегистрированные вычислители и бортовые компьютеры аппаратов.</p></div>
|
||||||
<div className="contour-health-summary__status"><StatusBadge tone={loading?'warning':summary.tone}>{loading?'Проверяем состояние':summary.label}</StatusBadge><Button size="compact" variant="secondary" disabled={loading} onClick={()=>setGeneration(value=>value+1)}><Icon name="refresh" size={14}/>{loading?'Проверяем':'Обновить'}</Button></div>
|
<div className="contour-health-summary__status"><StatusBadge tone={loading?'warning':summary.tone}>{loading?'Проверяем состояние':summary.label}</StatusBadge></div>
|
||||||
</section>
|
</section>
|
||||||
<section className="contour-health-kpis" aria-label="Сводка состояния контура">
|
<section className="contour-health-kpis" aria-label="Сводка состояния контура">
|
||||||
<div><span>Узлы на связи</span><strong>{summary.online??'—'} / {summary.total??'—'}</strong><small>Core, вычислители и бортовые ПК</small></div>
|
<div><span>Узлы в сети</span><strong>{summary.online??'—'} / {summary.total??'—'}</strong><small>Core, вычислители и бортовые ПК</small></div>
|
||||||
<div><span>Вычислители</span><strong>{snapshot?.workers?.length??'—'}</strong><small>из настроек системы</small></div>
|
<div><span>Вычислители</span><strong>{snapshot?.workers?.length??'—'}</strong><small>из настроек системы</small></div>
|
||||||
<div><span>Бортовые компьютеры</span><strong>{snapshot?.vehicles?.length??'—'}</strong><small>из реестра аппаратов</small></div>
|
<div><span>Бортовые компьютеры</span><strong>{snapshot?.vehicles?.length??'—'}</strong><small>из реестра аппаратов</small></div>
|
||||||
<div><span>Последняя проверка</span><strong>{observed(snapshot?.observedAt)}</strong><small>обновление каждые 5 секунд</small></div>
|
<div><span>Последняя проверка</span><strong>{observed(snapshot?.observedAt)}</strong><small>обновление каждые 5 секунд</small></div>
|
||||||
</section>
|
</section>
|
||||||
<section className="contour-node-grid" aria-label="Узлы контура">
|
<section className="contour-node-grid" aria-label="Узлы контура">
|
||||||
<article className="contour-node" data-state={core?.ok?'online':'offline'}>
|
<article className="contour-node" data-state={core?(core.ok?'online':'degraded'):'unknown'}>
|
||||||
<header><div><span className="section-eyebrow">КОМПЬЮТЕР ОПЕРАТОРА</span><h3>Mission Core</h3></div><StatusBadge tone={core?.ok?'success':core?'danger':'warning'}>{core?(core.ok?'Доступен':'Недоступен'):snapshot?'Нет свежих данных':'Проверяем'}</StatusBadge></header>
|
<header><div><span className="section-eyebrow">КОМПЬЮТЕР ОПЕРАТОРА</span><h3>Mission Core</h3></div><StatusBadge tone={core?.ok?'success':'warning'}>{core?'В сети':snapshot?'Не в сети':'Проверяем состояние'}</StatusBadge></header>
|
||||||
<dl><div><dt>Версия</dt><dd>{core?.version??'—'}</dd></div><div><dt>Время ответа</dt><dd>{snapshot?.latency==null?'—':`${Math.max(1,Math.round(snapshot.latency))} мс`}</dd></div><div><dt>Обработчики устройств готовы</dt><dd>{core?`${core.plugin_runtimes.ready} / ${core.plugin_runtimes.total}`:'—'}</dd></div></dl>
|
<dl><div><dt>Версия</dt><dd>{core?.version??'—'}</dd></div><div><dt>Время ответа</dt><dd>{snapshot?.latency==null?'—':`${Math.max(1,Math.round(snapshot.latency))} мс`}</dd></div><div><dt>Обработчики устройств готовы</dt><dd>{core?`${core.plugin_runtimes.ready} / ${core.plugin_runtimes.total}`:'—'}</dd></div></dl>
|
||||||
|
{!core?.ok&&core?.components?.recording_cache?.status==='capacity-pressure'&&<small>Недостаточно свободного места для новых записей.</small>}
|
||||||
</article>
|
</article>
|
||||||
{snapshot?.workers?.map(({contour,telemetry})=>{
|
{snapshot?.workers?.map(({contour,telemetry})=>{
|
||||||
const online=workerAvailable(telemetry);
|
const status=workerConnectionStatus(telemetry);
|
||||||
const mismatch=telemetry?.connection.reachable&&!telemetry.connection.identity_matches;
|
return <article key={contour.contour_id} className="contour-node" data-state={status.state}>
|
||||||
return <article key={contour.contour_id} className="contour-node" data-state={online?'online':'offline'}>
|
<header><div><span className="section-eyebrow">ВЫЧИСЛИТЕЛЬ</span><h3>{contour.display_name}</h3></div><StatusBadge tone={status.tone}>{status.label}</StatusBadge></header>
|
||||||
<header><div><span className="section-eyebrow">ВЫЧИСЛИТЕЛЬ</span><h3>{contour.display_name}</h3></div><StatusBadge tone={online?'success':!telemetry||mismatch?'warning':'neutral'}>{online?'На связи':mismatch?'Другой узел':telemetry?'Нет свежих данных':'Проверка недоступна'}</StatusBadge></header>
|
|
||||||
<dl><div><dt>Узел</dt><dd>{contour.expected_node_id}</dd></div><div><dt>Телеметрия</dt><dd>{contour.telemetry_mode==='agent-mqtt'?'Агент':'Диагностика SSH'}</dd></div><div><dt>Проверено</dt><dd>{observed(telemetry?.connection.observed_at_utc)}</dd></div><div><dt>Последнее измерение</dt><dd>{observed(telemetry?.node?.observed_at_utc)}</dd></div><div><dt>Операционная система</dt><dd>{telemetry?.node?.os.caption??'—'}</dd></div></dl>
|
<dl><div><dt>Узел</dt><dd>{contour.expected_node_id}</dd></div><div><dt>Телеметрия</dt><dd>{contour.telemetry_mode==='agent-mqtt'?'Агент':'Диагностика SSH'}</dd></div><div><dt>Проверено</dt><dd>{observed(telemetry?.connection.observed_at_utc)}</dd></div><div><dt>Последнее измерение</dt><dd>{observed(telemetry?.node?.observed_at_utc)}</dd></div><div><dt>Операционная система</dt><dd>{telemetry?.node?.os.caption??'—'}</dd></div></dl>
|
||||||
|
{status.detail&&<small>{status.detail}</small>}
|
||||||
</article>;
|
</article>;
|
||||||
})}
|
})}
|
||||||
{snapshot?.vehicles?.map(vehicle=>{
|
{snapshot?.vehicles?.map(vehicle=>{
|
||||||
const online=boardAvailable(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 <article key={vehicle.id} className="contour-node" data-state={online?'online':'offline'}>
|
return <article key={vehicle.id} className="contour-node" data-state={online?'online':'offline'}>
|
||||||
<header><div><span className="section-eyebrow">БОРТОВОЙ КОМПЬЮТЕР</span><h3>{vehicle.name}</h3></div><StatusBadge tone={online?'success':'neutral'}>{status}</StatusBadge></header>
|
<header><div><span className="section-eyebrow">БОРТОВОЙ КОМПЬЮТЕР</span><h3>{vehicle.name}</h3></div><StatusBadge tone={online?'success':'neutral'}>{status}</StatusBadge></header>
|
||||||
<dl><div><dt>Имя БК</dt><dd>{vehicle.host?.hostname??'—'}</dd></div><div><dt>Операционная система</dt><dd>{vehicle.host?.os??'—'}</dd></div><div><dt>Последняя связь</dt><dd>{observed(vehicle.last_seen===null?null:vehicle.last_seen*1000)}</dd></div></dl>
|
<dl><div><dt>Имя БК</dt><dd>{vehicle.host?.hostname??'—'}</dd></div><div><dt>Операционная система</dt><dd>{vehicle.host?.os??'—'}</dd></div><div><dt>Последняя связь</dt><dd>{observed(vehicle.last_seen===null?null:vehicle.last_seen*1000)}</dd></div></dl>
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) {
|
|||||||
case "contour-health":
|
case "contour-health":
|
||||||
return (
|
return (
|
||||||
<ContourHealthWorkspace
|
<ContourHealthWorkspace
|
||||||
|
headerToolsHost={props.headerToolsHost}
|
||||||
state={props.state}
|
state={props.state}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Button,Icon,IconButton,ResourceRow,StatusBadge,Window} from '@nodedc/ui-react';
|
import {Button,Icon,IconButton,ResourceRow,StatusBadge,Window} from '@nodedc/ui-react';
|
||||||
import type {Vehicle} from '../../core/fleet/useFleet';
|
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}){
|
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);
|
const label=boardStatus(vehicle,enabled);
|
||||||
return <ResourceRow icon={<Icon name="apps"/>} title="Бортовой компьютер" description={vehicle.host?.hostname||'Mission Core Node'} metadata={`${label}${showingSavedDevices?' (показаны последние сохранённые устройства)':''}`} statusPlacement="leading"
|
return <ResourceRow icon={<Icon name="apps"/>} title="Бортовой компьютер" description={vehicle.host?.hostname||'Mission Core Node'} metadata={`${label}${showingSavedDevices?' (показаны последние сохранённые устройства)':''}`} statusPlacement="leading"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {workerConnectionStatus} from '../../core/system/workerConnectionStatus';
|
||||||
import {
|
import {
|
||||||
GlassSurface,
|
GlassSurface,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
@@ -77,9 +78,7 @@ export function ComputeModulesWorkspace() {
|
|||||||
const node = telemetry?.node ?? null;
|
const node = telemetry?.node ?? null;
|
||||||
const missionCoreRuntimes = telemetry?.runtimes.filter((runtime) => !runtime.external) ?? [];
|
const missionCoreRuntimes = telemetry?.runtimes.filter((runtime) => !runtime.external) ?? [];
|
||||||
const externalRuntimes = telemetry?.runtimes.filter((runtime) => runtime.external) ?? [];
|
const externalRuntimes = telemetry?.runtimes.filter((runtime) => runtime.external) ?? [];
|
||||||
const connected = Boolean(
|
const connectionStatus = workerConnectionStatus(telemetry);
|
||||||
telemetry?.connection.reachable && telemetry.connection.identity_matches && node,
|
|
||||||
);
|
|
||||||
const agentTelemetry = telemetry?.connection.source === "agent-mqtt";
|
const agentTelemetry = telemetry?.connection.source === "agent-mqtt";
|
||||||
const history = telemetry?.history ?? [];
|
const history = telemetry?.history ?? [];
|
||||||
const cpuPercent = node?.cpu.load_percent;
|
const cpuPercent = node?.cpu.load_percent;
|
||||||
@@ -105,14 +104,13 @@ export function ComputeModulesWorkspace() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="system-workspace__actions">
|
<div className="system-workspace__actions">
|
||||||
<StatusBadge tone={connected ? "success" : "danger"}>
|
<StatusBadge tone={connectionStatus.tone}>
|
||||||
{connected
|
{connectionStatus.label}
|
||||||
? agentTelemetry ? "Агент доступен" : "SSH-диагностика"
|
|
||||||
: "Нет свежих данных"}
|
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{connectionStatus.detail && <p role="status">{connectionStatus.detail}</p>}
|
||||||
{error ? (
|
{error ? (
|
||||||
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
|
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
|
||||||
<StatusBadge tone="warning">{error}</StatusBadge>
|
<StatusBadge tone="warning">{error}</StatusBadge>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {workerConnectionStatus} from '../../core/system/workerConnectionStatus';
|
||||||
import {
|
import {
|
||||||
GlassSurface,
|
GlassSurface,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
@@ -31,6 +32,7 @@ export function NetworkWorkspace() {
|
|||||||
const connected = Boolean(
|
const connected = Boolean(
|
||||||
telemetry?.connection.reachable && telemetry.connection.identity_matches,
|
telemetry?.connection.reachable && telemetry.connection.identity_matches,
|
||||||
);
|
);
|
||||||
|
const connectionStatus = workerConnectionStatus(telemetry);
|
||||||
const agentTelemetry = telemetry?.connection.source === "agent-mqtt";
|
const agentTelemetry = telemetry?.connection.source === "agent-mqtt";
|
||||||
const runtimeOnline = telemetry?.runtimes.some(
|
const runtimeOnline = telemetry?.runtimes.some(
|
||||||
(runtime) => !runtime.external && runtime.state === "running",
|
(runtime) => !runtime.external && runtime.state === "running",
|
||||||
@@ -43,17 +45,18 @@ export function NetworkWorkspace() {
|
|||||||
<span className="section-eyebrow">СИСТЕМА / СЕТЬ</span>
|
<span className="section-eyebrow">СИСТЕМА / СЕТЬ</span>
|
||||||
<h2>{selectedContour?.display_name ?? "Контур не выбран"}</h2>
|
<h2>{selectedContour?.display_name ?? "Контур не выбран"}</h2>
|
||||||
<p>
|
<p>
|
||||||
Фактический локальный маршрут и сетевые счётчики выбранного вычислительного
|
Фактический маршрут и сетевые счётчики выбранного вычислительного
|
||||||
контура. Адрес, транспорт и установка агента находятся в настройках контура.
|
контура. Адрес, транспорт и установка агента находятся в настройках контура.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="system-workspace__actions">
|
<div className="system-workspace__actions">
|
||||||
<StatusBadge tone={connected ? "success" : "danger"}>
|
<StatusBadge tone={connectionStatus.tone}>
|
||||||
{connected ? "Маршрут доступен" : "Нет свежих данных"}
|
{connectionStatus.label}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{connectionStatus.detail && <p role="status">{connectionStatus.detail}</p>}
|
||||||
{error ? (
|
{error ? (
|
||||||
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
|
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
|
||||||
<StatusBadge tone="warning">{error}</StatusBadge>
|
<StatusBadge tone="warning">{error}</StatusBadge>
|
||||||
@@ -109,7 +112,7 @@ export function NetworkWorkspace() {
|
|||||||
<small>127.0.0.1:8000</small>
|
<small>127.0.0.1:8000</small>
|
||||||
</div>
|
</div>
|
||||||
<i aria-hidden="true" data-state="online" />
|
<i aria-hidden="true" data-state="online" />
|
||||||
<div data-state={agentTelemetry ? "online" : "offline"}>
|
<div data-state={connected ? "online" : "unknown"}>
|
||||||
<span>Telemetry plane</span>
|
<span>Telemetry plane</span>
|
||||||
<strong>{agentTelemetry ? "Mosquitto + Timescale" : "SSH diagnostic"}</strong>
|
<strong>{agentTelemetry ? "Mosquitto + Timescale" : "SSH diagnostic"}</strong>
|
||||||
<small>
|
<small>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ test('actual registries determine node count and offline boards prevent all-onli
|
|||||||
const calls=installFetch(t);
|
const calls=installFetch(t);
|
||||||
const snapshot=await readContourSnapshot(new AbortController().signal);
|
const snapshot=await readContourSnapshot(new AbortController().signal);
|
||||||
assert.equal(snapshot.workers[0].contour.display_name,'Test compute');
|
assert.equal(snapshot.workers[0].contour.display_name,'Test compute');
|
||||||
assert.deepEqual(contourSummary(snapshot),{tone:'warning',label:'Часть узлов не на связи',online:2,total:3});
|
assert.deepEqual(contourSummary(snapshot),{tone:'warning',label:'Часть узлов не в сети',online:2,total:3});
|
||||||
assert.equal(calls.length,4);
|
assert.equal(calls.length,4);
|
||||||
});
|
});
|
||||||
test('failed checks discard previous online data and distinguish unknown from empty',async t=>{
|
test('failed checks discard previous online data and distinguish unknown from empty',async t=>{
|
||||||
@@ -54,5 +54,24 @@ test('invalid health never becomes reachable; identity mismatch and stale worker
|
|||||||
});
|
});
|
||||||
test('an empty configured contour contains only the operator, with no fabricated worker',async t=>{
|
test('an empty configured contour contains only the operator, with no fabricated worker',async t=>{
|
||||||
installFetch(t,{data:{'/api/v1/system/contours':{schema_version:'missioncore.compute-contour-catalog/v1',contours:[]},'/api/v1/fleet':{items:[]}}});
|
installFetch(t,{data:{'/api/v1/system/contours':{schema_version:'missioncore.compute-contour-catalog/v1',contours:[]},'/api/v1/fleet':{items:[]}}});
|
||||||
assert.deepEqual(contourSummary(await readContourSnapshot(new AbortController().signal)),{tone:'success',label:'Все узлы на связи',online:1,total:1});
|
assert.deepEqual(contourSummary(await readContourSnapshot(new AbortController().signal)),{tone:'success',label:'Все узлы в сети',online:1,total:1});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('receiver failure leaves worker unknown instead of declaring it offline',async t=>{
|
||||||
|
installFetch(t,{data:{'/api/v1/system/contours/test-compute/telemetry?history=90':{...worker,node:null,connection:{reachable:false,identity_matches:false,error_code:'telemetry-receiver-unavailable'}}}});
|
||||||
|
const snapshot=await readContourSnapshot(new AbortController().signal);
|
||||||
|
assert.equal(contourSummary(snapshot).label,'Состояние части узлов не подтверждено');
|
||||||
|
const {workerConnectionStatus}=await server.ssrLoadModule('/src/core/system/workerConnectionStatus.ts');
|
||||||
|
const status=workerConnectionStatus(snapshot.workers[0].telemetry);
|
||||||
|
assert.equal(status.state,'unknown');
|
||||||
|
assert.equal(status.label,'Не в сети');
|
||||||
|
assert.equal(workerConnectionStatus({...worker,node:null,connection:{reachable:false,error_code:'telemetry-agent-stale'}}).label,'Не в сети');
|
||||||
|
assert.equal(workerConnectionStatus(worker).state,'online');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a responding Core with recording capacity pressure remains reachable',async t=>{
|
||||||
|
installFetch(t,{data:{'/api/health':{...core,ok:false,components:{recording_cache:{status:'capacity-pressure'}}}}});
|
||||||
|
const snapshot=await readContourSnapshot(new AbortController().signal);
|
||||||
|
assert.equal(contourSummary(snapshot).online,2);
|
||||||
|
assert.equal(contourSummary(snapshot).label,'Mission Core работает с ограничениями');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -722,7 +722,7 @@ def run_worker_agent_probe(
|
|||||||
):
|
):
|
||||||
return _failed_probe(
|
return _failed_probe(
|
||||||
profile,
|
profile,
|
||||||
"telemetry-agent-unavailable",
|
"telemetry-receiver-unavailable",
|
||||||
started,
|
started,
|
||||||
source="agent-mqtt",
|
source="agent-mqtt",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -497,3 +497,19 @@ def test_profile_apply_fails_closed_on_wrong_node_and_keeps_old_profile(
|
|||||||
assert error.value.status_code == 409
|
assert error.value.status_code == 409
|
||||||
assert WorkerProfileStore(tmp_path / "system").read().revision == 0
|
assert WorkerProfileStore(tmp_path / "system").read().revision == 0
|
||||||
assert not WorkerProfileStore(tmp_path / "system").path.exists()
|
assert not WorkerProfileStore(tmp_path / "system").path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_failure_is_not_reported_as_worker_failure(monkeypatch):
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
from k1link.web import system_telemetry_api as api
|
||||||
|
|
||||||
|
def unavailable(*args, **kwargs):
|
||||||
|
raise urllib.error.URLError("receiver down")
|
||||||
|
|
||||||
|
monkeypatch.setattr(api.urllib.request, "urlopen", unavailable)
|
||||||
|
result = api.run_worker_agent_probe(
|
||||||
|
api._profile_from_compute_contour(default_compute_contour())
|
||||||
|
)
|
||||||
|
assert result["error_code"] == "telemetry-receiver-unavailable"
|
||||||
|
assert result["reachable"] is False and result["raw"] is None
|
||||||
|
|||||||
Reference in New Issue
Block a user