feat(system): add Worker 006 telemetry and network profile

This commit is contained in:
DCCONSTRUCTIONS
2026-07-27 21:06:51 +03:00
parent 438196cd6d
commit 67e12a47a4
17 changed files with 2723 additions and 8 deletions
+4
View File
@@ -795,6 +795,10 @@ export default function App() {
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
) : activeDefinition.kind === "lab-archive" ? (
null
) : activeDefinition.kind === "compute-modules" ? (
<StatusBadge tone="neutral">Живая телеметрия</StatusBadge>
) : activeDefinition.kind === "network-monitor" ? (
<StatusBadge tone="neutral">Живая сеть</StatusBadge>
) : (
<StatusBadge tone="warning">Интерфейс готов</StatusBadge>
)
@@ -0,0 +1,47 @@
interface TelemetrySeriesProps {
label: string;
values: Array<number | null>;
value: string;
ceiling?: number;
}
function linePoints(values: Array<number | null>, ceiling?: number): string {
const finite = values.filter((value): value is number =>
value !== null && Number.isFinite(value),
);
if (finite.length === 0) return "";
const maximum = Math.max(ceiling ?? 0, ...finite, 1);
const denominator = Math.max(1, values.length - 1);
return values.map((value, index) => {
const x = index / denominator * 100;
const normalized = value === null ? 0 : Math.max(0, Math.min(maximum, value));
const y = 36 - normalized / maximum * 34;
return `${x.toFixed(2)},${y.toFixed(2)}`;
}).join(" ");
}
export function TelemetrySeries({
label,
values,
value,
ceiling,
}: TelemetrySeriesProps) {
const points = linePoints(values, ceiling);
return (
<div className="system-telemetry-series">
<div>
<span>{label}</span>
<strong>{value}</strong>
</div>
<svg
viewBox="0 0 100 38"
preserveAspectRatio="none"
role="img"
aria-label={`${label}: ${value}`}
>
<path d="M0 37 H100" />
{points ? <polyline points={points} /> : null}
</svg>
</div>
);
}
@@ -0,0 +1,50 @@
import { GlassSurface, StatusBadge } from "@nodedc/ui-react";
import type { WorkerRuntime } from "../../core/system/workerTelemetry";
import {
formatOptionalPercent,
runtimeStateLabel,
runtimeTone,
} from "./systemFormat";
export function WorkerRuntimeCard({ runtime }: { runtime: WorkerRuntime }) {
return (
<GlassSurface
className="worker-runtime-card"
padding="md"
tone={runtime.external ? "soft" : "default"}
data-external={runtime.external ? "true" : undefined}
>
<header>
<div>
<span className="section-eyebrow">
{runtime.external ? "ВНЕШНЯЯ НАГРУЗКА" : "MISSION CORE RUNTIME"}
</span>
<h3>{runtime.role}</h3>
<code>{runtime.name}</code>
</div>
<StatusBadge tone={runtimeTone(runtime)}>
{runtimeStateLabel(runtime)}
</StatusBadge>
</header>
<dl>
<div>
<dt>CPU</dt>
<dd>{formatOptionalPercent(runtime.cpu_percent)}</dd>
</div>
<div>
<dt>Память</dt>
<dd>{runtime.memory_usage ?? "—"}</dd>
</div>
<div>
<dt>Сеть I/O</dt>
<dd>{runtime.network_io ?? "—"}</dd>
</div>
<div>
<dt>Процессы</dt>
<dd>{runtime.pids ?? "—"}</dd>
</div>
</dl>
</GlassSurface>
);
}
@@ -0,0 +1,67 @@
import type { StatusTone } from "@nodedc/ui-react";
import type { WorkerRuntime } from "../../core/system/workerTelemetry";
export function formatOptionalPercent(value: number | null | undefined): string {
return typeof value === "number" && Number.isFinite(value)
? `${new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 1 }).format(value)}%`
: "—";
}
export function formatBytes(value: number | null | undefined): string {
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
const units = ["Б", "КиБ", "МиБ", "ГиБ", "ТиБ"];
let current = Math.max(0, value);
let unit = 0;
while (current >= 1024 && unit < units.length - 1) {
current /= 1024;
unit += 1;
}
return `${new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: current >= 100 ? 0 : 1,
}).format(current)} ${units[unit]}`;
}
export function formatRate(value: number | null | undefined): string {
const formatted = formatBytes(value);
return formatted === "—" ? formatted : `${formatted}/с`;
}
export function formatBitRate(value: number | null | undefined): string {
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
const units = ["бит/с", "Кбит/с", "Мбит/с", "Гбит/с"];
let current = Math.max(0, value);
let unit = 0;
while (current >= 1000 && unit < units.length - 1) {
current /= 1000;
unit += 1;
}
return `${new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: current >= 100 ? 0 : 1,
}).format(current)} ${units[unit]}`;
}
export function formatDuration(value: number | null | undefined): string {
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
const hours = Math.floor(value / 3600);
if (hours < 24) return `${hours} ч`;
return `${Math.floor(hours / 24)} д ${hours % 24} ч`;
}
export function formatLatency(value: number | null | undefined): string {
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
return `${Math.round(value)} мс`;
}
export function runtimeTone(runtime: WorkerRuntime): StatusTone {
if (runtime.state !== "running") return "danger";
if (runtime.health && runtime.health !== "healthy") return "warning";
return runtime.external ? "neutral" : "success";
}
export function runtimeStateLabel(runtime: WorkerRuntime): string {
if (runtime.state !== "running") return "Недоступен";
if (runtime.health === "healthy") return "Работает";
if (runtime.health) return runtime.health;
return "Запущен";
}
@@ -0,0 +1,47 @@
import { useCallback, useEffect, useState } from "react";
import {
fetchWorkerTelemetry,
type WorkerTelemetry,
} from "./workerTelemetry";
export interface WorkerTelemetryState {
telemetry: WorkerTelemetry | null;
loading: boolean;
error: string | null;
refresh: () => void;
}
export function useWorkerTelemetry(pollMilliseconds = 10_000): WorkerTelemetryState {
const [telemetry, setTelemetry] = useState<WorkerTelemetry | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [generation, setGeneration] = useState(0);
const refresh = useCallback(() => setGeneration((value) => value + 1), []);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
void fetchWorkerTelemetry(controller.signal)
.then((document) => {
if (controller.signal.aborted) return;
setTelemetry(document);
setError(null);
})
.catch((reason: unknown) => {
if (controller.signal.aborted) return;
setError(reason instanceof Error ? reason.message : "Worker 006 не ответил.");
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [generation]);
useEffect(() => {
const timer = window.setInterval(refresh, pollMilliseconds);
return () => window.clearInterval(timer);
}, [pollMilliseconds, refresh]);
return { telemetry, loading, error, refresh };
}
@@ -0,0 +1,249 @@
export interface WorkerConnectionProfile {
schema_version: "missioncore.worker-connection-profile/v1";
profile_id: string;
display_name: string;
expected_node_id: string;
ssh_host_alias: string;
address: string;
port: number;
revision: number;
updated_at_utc: string | null;
}
export interface WorkerProbe {
schema_version: "missioncore.worker-probe/v1";
reachable: boolean;
identity_matches: boolean;
node_id: string | null;
latency_ms: number | null;
observed_at_utc: string;
error_code: string | null;
}
export interface WorkerProfileDocument {
schema_version: "missioncore.worker-connection-profile/v1";
profile: WorkerConnectionProfile;
security: {
transport: "ssh";
host_key_policy: "strict-pinned";
credentials_managed_by_ui: false;
ssh_host_alias: string;
};
}
export interface WorkerRuntime {
name: string;
role: string;
owner: "mission-core" | "external";
external: boolean;
image: string | null;
state: string;
health: string | null;
cpu_percent: number | null;
memory_percent: number | null;
memory_usage: string | null;
network_io: string | null;
block_io: string | null;
pids: number | null;
}
export interface WorkerPipelineStage {
id: string;
label: string;
state: "active" | "waiting" | "ready" | "unavailable";
}
export interface WorkerTelemetryHistoryRow {
observed_at_utc: string;
cpu_percent: number | null;
memory_percent: number | null;
gpu_percent: number | null;
gpu_memory_percent: number | null;
network_receive_bytes_per_second: number | null;
network_send_bytes_per_second: number | null;
}
export interface WorkerNetworkInterface {
name: string;
description: string | null;
status: string | null;
link_speed_bps: number | null;
mac_address: string | null;
addresses: string[];
received_bytes: number | null;
sent_bytes: number | null;
}
export interface WorkerTelemetry {
schema_version: "missioncore.worker-telemetry/v1";
profile: WorkerConnectionProfile;
connection: WorkerProbe;
node: {
node_id: string;
observed_at_utc: string;
os: {
caption?: string;
version?: string;
uptime_seconds?: number;
};
cpu: {
name?: string;
logical_processors?: number;
load_percent?: number;
};
memory: {
total_bytes: number | null;
used_bytes: number | null;
free_bytes: number | null;
used_percent: number | null;
};
disks: Array<{
name?: string;
size_bytes?: number;
free_bytes?: number;
}>;
gpu: {
name?: string;
utilization_percent?: number;
memory_used_mib?: number;
memory_total_mib?: number;
power_watts?: number;
temperature_celsius?: number;
memory_used_percent?: number;
} | null;
triton: {
ready: boolean;
requests_succeeded: number | null;
requests_failed: number | null;
inferences: number | null;
};
} | null;
runtimes: WorkerRuntime[];
pipeline: {
service_state: string;
current_stage: string | null;
active_request_id: string | null;
active_frame_index: number | null;
completed_runs: number | null;
failed_runs: number | null;
model_load_seconds: number | null;
stages: WorkerPipelineStage[];
};
network: {
interfaces: WorkerNetworkInterface[];
aggregate: {
received_bytes: number;
sent_bytes: number;
receive_bytes_per_second: number | null;
send_bytes_per_second: number | null;
} | null;
};
history: WorkerTelemetryHistoryRow[];
}
export interface WorkerProfileMutation {
revision: number;
address: string;
port: number;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function requireJsonRecord(value: unknown, schema: string): Record<string, unknown> {
if (!isRecord(value) || value.schema_version !== schema) {
throw new Error("Ответ системной телеметрии не соответствует контракту.");
}
return value;
}
async function requestJson(
url: string,
init: RequestInit,
): Promise<unknown> {
const response = await fetch(url, {
...init,
headers: {
Accept: "application/json",
...(init.body ? { "Content-Type": "application/json" } : {}),
...init.headers,
},
});
if (!response.ok) {
throw new Error(`Системный API вернул HTTP ${response.status}.`);
}
return response.json();
}
export async function fetchWorkerTelemetry(
signal?: AbortSignal,
): Promise<WorkerTelemetry> {
const document = requireJsonRecord(
await requestJson("/api/v1/system/worker-telemetry?history=90", {
method: "GET",
signal,
}),
"missioncore.worker-telemetry/v1",
);
if (
!isRecord(document.connection)
|| !isRecord(document.profile)
|| !Array.isArray(document.runtimes)
|| !isRecord(document.pipeline)
|| !isRecord(document.network)
|| !Array.isArray(document.history)
) {
throw new Error("Срез Worker 006 неполон.");
}
return document as unknown as WorkerTelemetry;
}
export async function fetchWorkerProfile(
signal?: AbortSignal,
): Promise<WorkerProfileDocument> {
const document = requireJsonRecord(
await requestJson("/api/v1/system/worker-profile", {
method: "GET",
signal,
}),
"missioncore.worker-connection-profile/v1",
);
if (!isRecord(document.profile) || !isRecord(document.security)) {
throw new Error("Профиль Worker 006 неполон.");
}
return document as unknown as WorkerProfileDocument;
}
export async function testWorkerProfile(
request: WorkerProfileMutation,
signal?: AbortSignal,
): Promise<WorkerProbe> {
const document = requireJsonRecord(
await requestJson("/api/v1/system/worker-profile/test", {
method: "POST",
body: JSON.stringify(request),
signal,
}),
"missioncore.worker-probe/v1",
);
return document as unknown as WorkerProbe;
}
export async function saveWorkerProfile(
request: WorkerProfileMutation,
signal?: AbortSignal,
): Promise<WorkerProfileDocument & { verification: WorkerProbe }> {
const document = requireJsonRecord(
await requestJson("/api/v1/system/worker-profile", {
method: "PUT",
body: JSON.stringify(request),
signal,
}),
"missioncore.worker-connection-profile/v1",
);
if (!isRecord(document.profile) || !isRecord(document.verification)) {
throw new Error("Сохранённый профиль Worker 006 неполон.");
}
return document as unknown as WorkerProfileDocument & { verification: WorkerProbe };
}
+9 -5
View File
@@ -18,6 +18,8 @@ export type WorkspaceKind =
| "missions"
| "catalog"
| "contour-health"
| "compute-modules"
| "network-monitor"
| "datasets"
| "lab-archive";
@@ -589,16 +591,17 @@ export const workspaces: WorkspaceDefinition[] = [
label: "Вычислительные модули",
title: "Вычислительные модули",
eyebrow: "СИСТЕМА / МОДУЛИ",
description: "Контракты будущих вычислительных модулей без их запуска на текущем интерфейсном этапе.",
description: "Живая аппаратная и процессинговая телеметрия выделенного Worker 006.",
icon: "apps",
kind: "catalog",
kind: "compute-modules",
groups: [
{
title: "Вычислительные роли",
description: "Каждый модуль публикует стандартные выходы, интерфейс только отображает их.",
capabilities: [
active("Адаптер устройства", "Закрытый протокол текущего устройства уже изолирован."),
contract("Восприятие", "Рамки, маски, ключевые точки, траектории и семантические подписи."),
active("Восприятие", "Persistent perception worker и его текущие стадии обработки."),
active("Inference runtime", "Triton health, inference-счётчики и потребление ресурсов."),
contract("Картирование", "Высота, занятость, сетки и проходимость."),
contract("Локализация", "Поза, одометрия, TF и неопределённость."),
contract("Исполнитель миссий", "Состояние графа миссии и действия верхнего уровня."),
@@ -637,13 +640,14 @@ export const workspaces: WorkspaceDefinition[] = [
eyebrow: "СИСТЕМА / СЕТЬ",
description: "Локальный контур, бортовая сеть, ячеистая сеть/LTE и серверные маршруты.",
icon: "globe",
kind: "catalog",
kind: "network-monitor",
groups: [
{
title: "Сетевые зоны",
description: "Потоки управления и высокой пропускной способности разделяются архитектурно.",
capabilities: [
active("Локальная сеть", "Устройство и рабочая станция в одной доступной сети."),
active("Переносимый профиль", "Адрес и порт Worker 006 проверяются до применения."),
active("Сетевая телеметрия", "Интерфейсы, счётчики, throughput и фактический маршрут."),
active("Локальное управление", "Точка приёма учётных данных и браузерный контур управления ограничены локальным хостом."),
contract("Бортовая сеть", "Сенсоры, вычислитель и маршрутизатор внутри аппарата."),
contract("Дальний транспорт", "Ячеистая сеть или LTE передаёт предпросмотр, позу, события и изменения карты."),
+1
View File
@@ -11,3 +11,4 @@
@import "./styles/responsive.css";
@import "./styles/observation.css";
@import "./styles/environment-settings.css";
@import "./styles/system-telemetry.css";
@@ -0,0 +1,544 @@
.system-workspace {
display: grid;
min-width: 0;
gap: 1rem;
padding-bottom: 1rem;
}
.system-workspace__lead,
.system-section-heading {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 1.2rem;
}
.system-workspace__lead {
padding: 0.25rem 0 0.35rem;
}
.system-workspace__lead h2,
.system-section-heading h3 {
margin: 0.4rem 0 0;
color: var(--nodedc-text-primary);
letter-spacing: -0.035em;
}
.system-workspace__lead h2 {
font-size: 1.42rem;
}
.system-section-heading h3 {
font-size: 1.02rem;
}
.system-workspace__lead p,
.system-section-heading p {
max-width: 48rem;
margin: 0.42rem 0 0;
color: var(--nodedc-text-muted);
font-size: 0.68rem;
line-height: 1.48;
}
.system-workspace__actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 0.8rem;
}
.system-workspace__notice {
display: flex;
align-items: center;
}
.system-telemetry-grid,
.network-overview-grid {
display: grid;
min-width: 0;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.65rem;
}
.system-telemetry-series,
.network-stat-card {
min-width: 0;
border: 1px solid var(--station-hairline);
border-radius: var(--nodedc-radius-card);
background: var(--station-panel-soft);
}
.system-telemetry-series {
overflow: hidden;
padding: 0.82rem 0.82rem 0.32rem;
}
.system-telemetry-series > div {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
}
.system-telemetry-series span,
.network-stat-card span {
color: var(--nodedc-text-muted);
font-size: 0.62rem;
}
.system-telemetry-series strong,
.network-stat-card strong {
color: var(--nodedc-text-primary);
font-size: 1.14rem;
font-weight: 720;
letter-spacing: -0.035em;
}
.system-telemetry-series svg {
display: block;
width: 100%;
height: 2.45rem;
margin-top: 0.45rem;
}
.system-telemetry-series path {
fill: none;
stroke: var(--station-hairline);
stroke-width: 0.8;
vector-effect: non-scaling-stroke;
}
.system-telemetry-series polyline {
fill: none;
stroke: var(--nodedc-text-primary);
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.15;
vector-effect: non-scaling-stroke;
}
.worker-hardware,
.worker-pipeline,
.network-profile,
.network-topology {
display: grid;
min-width: 0;
gap: 1rem;
}
.worker-hardware__facts {
display: grid;
min-width: 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.7rem;
}
.worker-hardware__facts dl,
.worker-runtime-card dl,
.network-interface-card dl {
display: grid;
min-width: 0;
margin: 0;
gap: 0;
}
.worker-hardware__facts dl {
grid-template-columns: repeat(2, minmax(0, 1fr));
border: 1px solid var(--station-hairline);
border-radius: var(--nodedc-radius-control);
}
.worker-hardware__facts dl > div,
.worker-runtime-card dl > div,
.network-interface-card dl > div {
display: grid;
min-width: 0;
gap: 0.22rem;
}
.worker-hardware__facts dl > div {
padding: 0.75rem;
}
.worker-hardware__facts dt,
.worker-runtime-card dt,
.network-interface-card dt,
.network-profile__security dt {
color: var(--nodedc-text-muted);
font-size: 0.58rem;
}
.worker-hardware__facts dd,
.worker-runtime-card dd,
.network-interface-card dd,
.network-profile__security dd {
min-width: 0;
margin: 0;
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.67rem;
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
.worker-disk-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
gap: 0.55rem;
}
.worker-disk-list > div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
padding: 0.62rem 0.72rem;
border-radius: var(--nodedc-radius-control);
background: var(--station-accent-soft);
}
.worker-disk-list span {
color: var(--nodedc-text-muted);
font-size: 0.61rem;
}
.worker-disk-list strong {
color: var(--nodedc-text-primary);
font-size: 0.64rem;
}
.system-runtime-section {
display: grid;
min-width: 0;
gap: 0.75rem;
}
.system-runtime-section--external {
padding-top: 0.25rem;
border-top: 1px solid var(--station-hairline);
}
.worker-runtime-grid {
display: grid;
min-width: 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.7rem;
}
.worker-runtime-card {
display: grid;
min-width: 0;
gap: 0.9rem;
}
.worker-runtime-card[data-external="true"] {
opacity: 0.78;
}
.worker-runtime-card > header,
.network-interface-card > header {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 0.8rem;
}
.worker-runtime-card h3 {
margin: 0.38rem 0 0.18rem;
color: var(--nodedc-text-primary);
font-size: 0.9rem;
letter-spacing: -0.025em;
}
.worker-runtime-card code {
display: block;
max-width: 23rem;
overflow: hidden;
color: var(--nodedc-text-muted);
font-size: 0.56rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.worker-runtime-card dl {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.worker-runtime-card dl > div {
padding-right: 0.65rem;
}
.worker-pipeline__summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.55rem;
}
.worker-pipeline__summary > div {
display: grid;
gap: 0.32rem;
padding: 0.7rem;
border-radius: var(--nodedc-radius-control);
background: var(--station-panel-soft);
}
.worker-pipeline__summary span {
color: var(--nodedc-text-muted);
font-size: 0.57rem;
}
.worker-pipeline__summary strong {
color: var(--nodedc-text-primary);
font-size: 0.78rem;
}
.worker-stage-list {
display: grid;
min-width: 0;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 0;
padding: 0;
gap: 0.48rem;
list-style: none;
}
.worker-stage-list li {
display: grid;
min-width: 0;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.55rem;
padding: 0.66rem;
border: 1px solid var(--station-hairline);
border-radius: var(--nodedc-radius-control);
background: var(--station-panel-soft);
}
.worker-stage-list li[data-state="active"] {
border-color: var(--station-hairline-strong);
background: var(--nodedc-focus-surface);
}
.worker-stage-list li > span,
.worker-stage-list li > small {
color: var(--nodedc-text-muted);
font-size: 0.53rem;
}
.worker-stage-list li > strong {
min-width: 0;
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.62rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.network-stat-card {
display: grid;
align-content: center;
gap: 0.3rem;
padding: 0.82rem;
}
.network-stat-card small {
overflow: hidden;
color: var(--nodedc-text-muted);
font-size: 0.55rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.network-profile__form {
display: grid;
min-width: 0;
grid-template-columns: minmax(12rem, 1.5fr) minmax(8rem, 0.5fr) auto;
align-items: end;
gap: 0.7rem;
}
.network-profile__buttons {
display: flex;
align-items: center;
gap: 0.5rem;
padding-bottom: 0.02rem;
}
.network-profile__security {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin: 0;
gap: 0.55rem;
}
.network-profile__security > div {
display: grid;
min-width: 0;
gap: 0.24rem;
padding: 0.64rem 0.7rem;
border-radius: var(--nodedc-radius-control);
background: var(--station-accent-soft);
}
.network-route {
display: grid;
min-width: 0;
grid-template-columns: repeat(3, minmax(8rem, 1fr) 2.2rem) minmax(8rem, 1fr);
align-items: center;
}
.network-route > div {
display: grid;
min-width: 0;
gap: 0.3rem;
padding: 0.78rem;
border: 1px solid var(--station-hairline);
border-radius: var(--nodedc-radius-control);
background: var(--station-panel-soft);
}
.network-route > div[data-state="offline"] {
opacity: 0.48;
}
.network-route > div span,
.network-route > div small {
overflow: hidden;
color: var(--nodedc-text-muted);
font-size: 0.55rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.network-route > div strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.68rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.network-route > i {
height: 1px;
background: var(--station-hairline-strong);
}
.network-route > i[data-state="offline"] {
background: var(--station-hairline);
opacity: 0.4;
}
.network-interface-section {
display: grid;
gap: 0.75rem;
}
.network-interface-list {
display: grid;
min-width: 0;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.65rem;
}
.network-interface-card {
display: grid;
min-width: 0;
gap: 0.85rem;
}
.network-interface-card header strong,
.network-interface-card header small {
display: block;
max-width: 17rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.network-interface-card header strong {
color: var(--nodedc-text-primary);
font-size: 0.71rem;
}
.network-interface-card header small {
margin-top: 0.25rem;
color: var(--nodedc-text-muted);
font-size: 0.55rem;
}
.network-interface-card dl {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
}
@media (max-width: 1100px) {
.system-telemetry-grid,
.network-overview-grid,
.worker-pipeline__summary,
.network-profile__security {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.worker-stage-list,
.network-interface-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.network-profile__form {
grid-template-columns: minmax(12rem, 1fr) 8rem;
}
.network-profile__buttons {
grid-column: 1 / -1;
}
.network-route {
grid-template-columns: 1fr;
gap: 0.4rem;
}
.network-route > i {
width: 1px;
height: 1.2rem;
margin-left: 1.1rem;
}
}
@media (max-width: 760px) {
.system-workspace__lead,
.system-section-heading {
display: grid;
}
.system-workspace__actions {
justify-content: space-between;
}
.system-telemetry-grid,
.network-overview-grid,
.worker-hardware__facts,
.worker-runtime-grid,
.worker-stage-list,
.network-interface-list,
.network-profile__security {
grid-template-columns: 1fr;
}
.worker-runtime-card dl,
.worker-pipeline__summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.network-profile__form {
grid-template-columns: 1fr;
}
.network-profile__buttons {
grid-column: auto;
flex-wrap: wrap;
}
}
@@ -46,6 +46,8 @@ import type { WorkspaceRendererProps } from "./contracts";
import { DatasetGatewayWorkspace } from "./DatasetGatewayWorkspace";
import { ContourHealthWorkspace } from "./ContourHealthWorkspace";
import { LaboratoryArchiveWorkspace } from "./laboratory/LaboratoryArchiveWorkspace";
import { ComputeModulesWorkspace } from "./system/ComputeModulesWorkspace";
import { NetworkWorkspace } from "./system/NetworkWorkspace";
function statusTone(status: CapabilityStatus): "success" | "accent" | "warning" | "neutral" {
if (status === "active") return "success";
@@ -1177,6 +1179,10 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) {
state={props.state}
/>
);
case "compute-modules":
return <ComputeModulesWorkspace />;
case "network-monitor":
return <NetworkWorkspace />;
case "datasets":
return <DatasetGatewayWorkspace />;
case "lab-archive":
@@ -0,0 +1,226 @@
import {
Button,
GlassSurface,
Icon,
StatusBadge,
} from "@nodedc/ui-react";
import { TelemetrySeries } from "../../components/system/TelemetrySeries";
import { WorkerRuntimeCard } from "../../components/system/WorkerRuntimeCard";
import {
formatBytes,
formatDuration,
formatOptionalPercent,
} from "../../components/system/systemFormat";
import { useWorkerTelemetry } from "../../core/system/useWorkerTelemetry";
function pipelineStateLabel(state: string): string {
if (state === "busy") return "Выполняет задачу";
if (state === "ready") return "Готов к задаче";
return "Нет live-состояния";
}
export function ComputeModulesWorkspace() {
const { telemetry, loading, error, refresh } = useWorkerTelemetry();
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 history = telemetry?.history ?? [];
const cpuPercent = node?.cpu.load_percent;
const memoryPercent = node?.memory.used_percent;
const gpuPercent = node?.gpu?.utilization_percent;
const gpuMemoryPercent = node?.gpu?.memory_used_percent;
return (
<div className="system-workspace compute-modules-workspace">
<section className="system-workspace__lead">
<div>
<span className="section-eyebrow">СИСТЕМА / ВЫЧИСЛИТЕЛЬНЫЕ МОДУЛИ</span>
<h2>Worker 006</h2>
<p>
Живой аппаратный и процессинговый срез выделенного узла. Нагрузка внешних
сервисов отделена от Mission Core и не входит в оценку наших runtime.
</p>
</div>
<div className="system-workspace__actions">
<StatusBadge tone={connected ? "success" : "danger"}>
{connected ? "Узел доступен" : "Нет связи с узлом"}
</StatusBadge>
<Button
size="compact"
variant="secondary"
disabled={loading}
onClick={refresh}
icon={<Icon name="refresh" size={14} />}
>
{loading ? "Читаем" : "Обновить"}
</Button>
</div>
</section>
{error ? (
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
<StatusBadge tone="warning">{error}</StatusBadge>
</GlassSurface>
) : null}
<section className="system-telemetry-grid" aria-label="Аппаратная телеметрия">
<TelemetrySeries
label="CPU"
value={formatOptionalPercent(cpuPercent)}
ceiling={100}
values={history.map((item) => item.cpu_percent)}
/>
<TelemetrySeries
label="RAM"
value={formatOptionalPercent(memoryPercent)}
ceiling={100}
values={history.map((item) => item.memory_percent)}
/>
<TelemetrySeries
label="GPU"
value={formatOptionalPercent(gpuPercent)}
ceiling={100}
values={history.map((item) => item.gpu_percent)}
/>
<TelemetrySeries
label="VRAM"
value={formatOptionalPercent(gpuMemoryPercent)}
ceiling={100}
values={history.map((item) => item.gpu_memory_percent)}
/>
</section>
<GlassSurface className="worker-hardware" padding="lg">
<header className="system-section-heading">
<div>
<span className="section-eyebrow">HARDWARE</span>
<h3>{node?.node_id ?? telemetry?.profile.expected_node_id ?? "Worker 006"}</h3>
<p>{node?.os.caption ?? "Аппаратный профиль недоступен"}</p>
</div>
<StatusBadge tone={node?.node_id ? "success" : "danger"}>
{node?.node_id ? telemetry?.profile.display_name : "Нет данных"}
</StatusBadge>
</header>
<div className="worker-hardware__facts">
<dl>
<div><dt>Процессор</dt><dd>{node?.cpu.name ?? "—"}</dd></div>
<div><dt>Логические ядра</dt><dd>{node?.cpu.logical_processors ?? "—"}</dd></div>
<div><dt>Память занята</dt><dd>{formatBytes(node?.memory.used_bytes)}</dd></div>
<div><dt>Uptime</dt><dd>{formatDuration(node?.os.uptime_seconds)}</dd></div>
</dl>
<dl>
<div><dt>GPU</dt><dd>{node?.gpu?.name ?? "—"}</dd></div>
<div><dt>VRAM занята</dt><dd>{formatBytes(
typeof node?.gpu?.memory_used_mib === "number"
? node.gpu.memory_used_mib * 1024 * 1024
: null,
)}</dd></div>
<div><dt>Температура</dt><dd>{
typeof node?.gpu?.temperature_celsius === "number"
? `${node.gpu.temperature_celsius} °C`
: "—"
}</dd></div>
<div><dt>Мощность</dt><dd>{
typeof node?.gpu?.power_watts === "number"
? `${node.gpu.power_watts.toFixed(1)} Вт`
: "—"
}</dd></div>
</dl>
</div>
<div className="worker-disk-list">
{(node?.disks ?? []).map((disk) => {
const size = typeof disk.size_bytes === "number" ? disk.size_bytes : null;
const free = typeof disk.free_bytes === "number" ? disk.free_bytes : null;
const used = size !== null && free !== null ? size - free : null;
return (
<div key={disk.name ?? "disk"}>
<span>Диск {disk.name ?? "—"}</span>
<strong>{formatBytes(used)} / {formatBytes(size)}</strong>
</div>
);
})}
</div>
</GlassSurface>
<section className="system-runtime-section">
<header className="system-section-heading">
<div>
<span className="section-eyebrow">PROCESSING RUNTIME</span>
<h3>Контейнеры Mission Core</h3>
<p>Два ограниченных runtime: inference server и прикладной perception pipeline.</p>
</div>
<StatusBadge tone={node?.triton.ready ? "success" : "danger"}>
{node?.triton.ready ? "Triton ready" : "Triton недоступен"}
</StatusBadge>
</header>
<div className="worker-runtime-grid">
{missionCoreRuntimes.map((runtime) => (
<WorkerRuntimeCard key={runtime.name} runtime={runtime} />
))}
</div>
</section>
<GlassSurface className="worker-pipeline" padding="lg">
<header className="system-section-heading">
<div>
<span className="section-eyebrow">ТЕКУЩАЯ ЗАДАЧА</span>
<h3>{pipelineStateLabel(telemetry?.pipeline.service_state ?? "unavailable")}</h3>
<p>
{telemetry?.pipeline.active_request_id
? `Run ${telemetry.pipeline.active_request_id}`
: "Очередь свободна; модели остаются загруженными в persistent worker."}
</p>
</div>
<StatusBadge tone={
telemetry?.pipeline.service_state === "busy" ? "warning"
: telemetry?.pipeline.service_state === "ready" ? "success"
: "danger"
}>
{telemetry?.pipeline.service_state ?? "unavailable"}
</StatusBadge>
</header>
<div className="worker-pipeline__summary">
<div><span>Завершено запусков</span><strong>{telemetry?.pipeline.completed_runs ?? "—"}</strong></div>
<div><span>Ошибок запусков</span><strong>{telemetry?.pipeline.failed_runs ?? "—"}</strong></div>
<div><span>Inference success</span><strong>{node?.triton.requests_succeeded ?? "—"}</strong></div>
<div><span>Inference failed</span><strong>{node?.triton.requests_failed ?? "—"}</strong></div>
</div>
<ol className="worker-stage-list">
{(telemetry?.pipeline.stages ?? []).map((stage, index) => (
<li key={stage.id} data-state={stage.state}>
<span>{String(index + 1).padStart(2, "0")}</span>
<strong>{stage.label}</strong>
<small>{
stage.state === "active" ? "выполняется"
: stage.state === "waiting" ? "в очереди"
: stage.state === "ready" ? "готов"
: "нет данных"
}</small>
</li>
))}
</ol>
</GlassSurface>
<section className="system-runtime-section system-runtime-section--external">
<header className="system-section-heading">
<div>
<span className="section-eyebrow">CO-TENANTS / НЕ MISSION CORE</span>
<h3>Внешняя нагрузка узла</h3>
<p>
Эти процессы влияют на общую плату и VRAM, но не считаются расходом Mission Core.
</p>
</div>
</header>
<div className="worker-runtime-grid">
{externalRuntimes.map((runtime) => (
<WorkerRuntimeCard key={runtime.name} runtime={runtime} />
))}
</div>
</section>
</div>
);
}
@@ -0,0 +1,311 @@
import { useEffect, useMemo, useState } from "react";
import {
Button,
GlassSurface,
Icon,
StatusBadge,
TextField,
} from "@nodedc/ui-react";
import { TelemetrySeries } from "../../components/system/TelemetrySeries";
import {
formatBitRate,
formatBytes,
formatLatency,
formatRate,
} from "../../components/system/systemFormat";
import {
fetchWorkerProfile,
saveWorkerProfile,
testWorkerProfile,
type WorkerConnectionProfile,
type WorkerProbe,
} from "../../core/system/workerTelemetry";
import { useWorkerTelemetry } from "../../core/system/useWorkerTelemetry";
type ProfileAction = "idle" | "testing" | "saving";
function probeLabel(probe: WorkerProbe | null): string {
if (!probe) return "Изменения не проверены";
if (!probe.reachable) return "Адрес не отвечает";
if (!probe.identity_matches) return "Ответил другой узел";
return "Worker 006 подтверждён";
}
export function NetworkWorkspace() {
const { telemetry, loading, error, refresh } = useWorkerTelemetry();
const [profile, setProfile] = useState<WorkerConnectionProfile | null>(null);
const [address, setAddress] = useState("");
const [port, setPort] = useState("22");
const [profileAction, setProfileAction] = useState<ProfileAction>("idle");
const [probe, setProbe] = useState<WorkerProbe | null>(null);
const [profileError, setProfileError] = useState<string | null>(null);
const aggregate = telemetry?.network.aggregate ?? null;
const connected = Boolean(
telemetry?.connection.reachable && telemetry.connection.identity_matches,
);
useEffect(() => {
const controller = new AbortController();
void fetchWorkerProfile(controller.signal)
.then((document) => {
if (controller.signal.aborted) return;
setProfile(document.profile);
setAddress(document.profile.address);
setPort(String(document.profile.port));
setProfileError(null);
})
.catch((reason: unknown) => {
if (controller.signal.aborted) return;
setProfileError(reason instanceof Error ? reason.message : "Профиль не прочитан.");
});
return () => controller.abort();
}, []);
const mutation = useMemo(() => {
const numericPort = Number(port);
if (!profile || !Number.isInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
return null;
}
return {
revision: profile.revision,
address: address.trim(),
port: numericPort,
};
}, [address, port, profile]);
const runTest = () => {
if (!mutation) return;
const controller = new AbortController();
setProfileAction("testing");
setProfileError(null);
setProbe(null);
void testWorkerProfile(mutation, controller.signal)
.then(setProbe)
.catch((reason: unknown) => {
setProfileError(reason instanceof Error ? reason.message : "Проверка не выполнена.");
})
.finally(() => setProfileAction("idle"));
};
const saveProfile = () => {
if (!mutation) return;
const controller = new AbortController();
setProfileAction("saving");
setProfileError(null);
void saveWorkerProfile(mutation, controller.signal)
.then((document) => {
setProfile(document.profile);
setAddress(document.profile.address);
setPort(String(document.profile.port));
setProbe(document.verification);
refresh();
})
.catch((reason: unknown) => {
setProfileError(reason instanceof Error ? reason.message : "Профиль не сохранён.");
})
.finally(() => setProfileAction("idle"));
};
return (
<div className="system-workspace network-workspace">
<section className="system-workspace__lead">
<div>
<span className="section-eyebrow">СИСТЕМА / СЕТЬ</span>
<h2>Локальный вычислительный контур</h2>
<p>
Переносимый профиль связи между Mission Core и Worker 006. Адрес можно заменить
при переходе в другую локальную сеть; идентичность узла проверяется до сохранения.
</p>
</div>
<div className="system-workspace__actions">
<StatusBadge tone={connected ? "success" : "danger"}>
{connected ? "Маршрут доступен" : "Маршрут недоступен"}
</StatusBadge>
<Button
size="compact"
variant="secondary"
disabled={loading}
onClick={refresh}
icon={<Icon name="refresh" size={14} />}
>
{loading ? "Читаем" : "Обновить"}
</Button>
</div>
</section>
{(error || profileError) ? (
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
<StatusBadge tone="warning">{profileError ?? error}</StatusBadge>
</GlassSurface>
) : null}
<section className="network-overview-grid" aria-label="Сетевая телеметрия">
<TelemetrySeries
label="Приём"
value={formatRate(aggregate?.receive_bytes_per_second)}
values={(telemetry?.history ?? []).map(
(item) => item.network_receive_bytes_per_second,
)}
/>
<TelemetrySeries
label="Передача"
value={formatRate(aggregate?.send_bytes_per_second)}
values={(telemetry?.history ?? []).map(
(item) => item.network_send_bytes_per_second,
)}
/>
<div className="network-stat-card">
<span>Сбор телеметрии</span>
<strong>{formatLatency(telemetry?.connection.latency_ms)}</strong>
<small>полный SSH probe Worker 006</small>
</div>
<div className="network-stat-card">
<span>Активные интерфейсы</span>
<strong>{telemetry?.network.interfaces.length ?? "—"}</strong>
<small>по Windows network counters</small>
</div>
</section>
<GlassSurface className="network-profile" padding="lg">
<header className="system-section-heading">
<div>
<span className="section-eyebrow">ПРОФИЛЬ ПОДКЛЮЧЕНИЯ</span>
<h3>Worker 006</h3>
<p>
Реальный Node ID: <code>{profile?.expected_node_id ?? "DESKTOP-OPJ8J04"}</code>.
Пустой адрес использует закреплённый локальный SSH-профиль.
</p>
</div>
<StatusBadge tone={
probe?.reachable && probe.identity_matches ? "success"
: probe ? "danger"
: connected ? "success" : "neutral"
}>
{probe ? probeLabel(probe) : connected ? "Текущий профиль работает" : "Не проверено"}
</StatusBadge>
</header>
<div className="network-profile__form">
<TextField
label="Адрес Worker 006"
hint="IP или hostname"
value={address}
placeholder="из SSH-профиля mission-gpu"
autoComplete="off"
spellCheck={false}
onChange={(event) => {
setAddress(event.target.value);
setProbe(null);
}}
/>
<TextField
label="SSH-порт"
hint="165535"
type="number"
min={1}
max={65535}
value={port}
onChange={(event) => {
setPort(event.target.value);
setProbe(null);
}}
/>
<div className="network-profile__buttons">
<Button
variant="secondary"
disabled={!mutation || profileAction !== "idle"}
onClick={runTest}
>
{profileAction === "testing" ? "Проверяем" : "Проверить"}
</Button>
<Button
variant="primary"
disabled={!mutation || profileAction !== "idle"}
onClick={saveProfile}
>
{profileAction === "saving" ? "Сохраняем" : "Проверить и применить"}
</Button>
</div>
</div>
<dl className="network-profile__security">
<div><dt>Транспорт</dt><dd>SSH · key-only</dd></div>
<div><dt>Host key</dt><dd>strict · pinned</dd></div>
<div><dt>Профиль</dt><dd>{profile?.ssh_host_alias ?? "mission-gpu"}</dd></div>
<div><dt>Учётные данные</dt><dd>не доступны интерфейсу</dd></div>
</dl>
</GlassSurface>
<GlassSurface className="network-topology" padding="lg">
<header className="system-section-heading">
<div>
<span className="section-eyebrow">ФАКТИЧЕСКИЙ МАРШРУТ</span>
<h3>Поток управления и обработки</h3>
<p>Схема собрана из текущего профиля и live health runtime, без demo-узлов.</p>
</div>
</header>
<div className="network-route">
<div data-state="online">
<span>Операторский UI</span>
<strong>Browser</strong>
<small>127.0.0.1:8000</small>
</div>
<i aria-hidden="true" data-state="online" />
<div data-state="online">
<span>Control Plane</span>
<strong>Mission Core</strong>
<small>локальный API</small>
</div>
<i aria-hidden="true" data-state={connected ? "online" : "offline"} />
<div data-state={connected ? "online" : "offline"}>
<span>Вычислительный узел</span>
<strong>Worker 006</strong>
<small>{profile?.address || profile?.ssh_host_alias || "mission-gpu"}:{profile?.port ?? 22}</small>
</div>
<i aria-hidden="true" data-state={
telemetry?.runtimes.some((runtime) => !runtime.external && runtime.state === "running")
? "online" : "offline"
} />
<div data-state={
telemetry?.runtimes.some((runtime) => !runtime.external && runtime.state === "running")
? "online" : "offline"
}>
<span>Runtime</span>
<strong>Triton + Pipeline</strong>
<small>внутренний Docker-контур</small>
</div>
</div>
</GlassSurface>
<section className="network-interface-section">
<header className="system-section-heading">
<div>
<span className="section-eyebrow">ИНТЕРФЕЙСЫ WORKER 006</span>
<h3>Адаптеры и счётчики</h3>
<p>Только активные интерфейсы, которые вернул сам узел.</p>
</div>
</header>
<div className="network-interface-list">
{(telemetry?.network.interfaces ?? []).map((adapter) => (
<GlassSurface className="network-interface-card" padding="md" key={adapter.name}>
<header>
<div>
<strong>{adapter.name}</strong>
<small>{adapter.description ?? "Описание недоступно"}</small>
</div>
<StatusBadge tone={adapter.status === "Up" ? "success" : "warning"}>
{adapter.status ?? "unknown"}
</StatusBadge>
</header>
<dl>
<div><dt>Адреса</dt><dd>{adapter.addresses.join(", ") || "—"}</dd></div>
<div><dt>Link</dt><dd>{formatBitRate(adapter.link_speed_bps)}</dd></div>
<div><dt>Получено</dt><dd>{formatBytes(adapter.received_bytes)}</dd></div>
<div><dt>Отправлено</dt><dd>{formatBytes(adapter.sent_bytes)}</dd></div>
</dl>
</GlassSurface>
))}
</div>
</section>
</div>
);
}
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
const sourceRoot = new URL("../src/", import.meta.url);
async function read(relativePath) {
return readFile(new URL(relativePath, sourceRoot), "utf8");
}
test("Worker 006 telemetry remains a bounded system feature slice", async () => {
const [
productModel,
workspaceHub,
core,
computeWorkspace,
networkWorkspace,
styles,
] = await Promise.all([
read("productModel.ts"),
read("workspaces/Workspaces.tsx"),
read("core/system/workerTelemetry.ts"),
read("workspaces/system/ComputeModulesWorkspace.tsx"),
read("workspaces/system/NetworkWorkspace.tsx"),
read("styles.css"),
]);
assert.match(productModel, /kind: "compute-modules"/);
assert.match(productModel, /kind: "network-monitor"/);
assert.match(workspaceHub, /<ComputeModulesWorkspace \/>/);
assert.match(workspaceHub, /<NetworkWorkspace \/>/);
assert.doesNotMatch(workspaceHub, /worker-telemetry|worker-profile|DESKTOP-OPJ8J04/);
assert.match(core, /\/api\/v1\/system\/worker-telemetry/);
assert.match(core, /\/api\/v1\/system\/worker-profile/);
assert.doesNotMatch(core, /@nodedc\/ui-react/);
assert.match(computeWorkspace, /useWorkerTelemetry/);
assert.match(networkWorkspace, /127\.0\.0\.1:8000/);
assert.doesNotMatch(networkWorkspace, /8765/);
assert.match(styles, /system-telemetry\.css/);
});