feat(control-station): add compute contour workspace
This commit is contained in:
@@ -13,7 +13,11 @@ import {
|
||||
formatDuration,
|
||||
formatOptionalPercent,
|
||||
} from "../../components/system/systemFormat";
|
||||
import { useWorkerTelemetry } from "../../core/system/useWorkerTelemetry";
|
||||
import { useComputeContours } from "../../core/system/ComputeContourContext";
|
||||
import {
|
||||
useWorkerTelemetry,
|
||||
WORKER_TELEMETRY_POLL_MILLISECONDS,
|
||||
} from "../../core/system/useWorkerTelemetry";
|
||||
|
||||
function pipelineStateLabel(state: string): string {
|
||||
if (state === "busy") return "Выполняет задачу";
|
||||
@@ -22,13 +26,21 @@ function pipelineStateLabel(state: string): string {
|
||||
}
|
||||
|
||||
export function ComputeModulesWorkspace() {
|
||||
const { telemetry, loading, error, refresh } = useWorkerTelemetry();
|
||||
const { selectedContour } = useComputeContours();
|
||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||
const { telemetry, loading, error, refresh } = useWorkerTelemetry(
|
||||
WORKER_TELEMETRY_POLL_MILLISECONDS,
|
||||
supportsLiveTelemetry,
|
||||
);
|
||||
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 agentTelemetry = telemetry?.connection.source === "agent-mqtt";
|
||||
const history = telemetry?.history ?? [];
|
||||
const cpuPercent = node?.cpu.load_percent;
|
||||
const memoryPercent = node?.memory.used_percent;
|
||||
@@ -40,15 +52,17 @@ export function ComputeModulesWorkspace() {
|
||||
<section className="system-workspace__lead">
|
||||
<div>
|
||||
<span className="section-eyebrow">СИСТЕМА / ВЫЧИСЛИТЕЛЬНЫЕ МОДУЛИ</span>
|
||||
<h2>Worker 006</h2>
|
||||
<h2>{selectedContour?.display_name ?? "Контур не выбран"}</h2>
|
||||
<p>
|
||||
Живой аппаратный и процессинговый срез выделенного узла. Нагрузка внешних
|
||||
сервисов отделена от Mission Core и не входит в оценку наших runtime.
|
||||
Аппаратный и процессинговый срез выбранного узла. Нагрузка внешних сервисов
|
||||
отделена от Mission Core и не входит в оценку наших runtime.
|
||||
</p>
|
||||
</div>
|
||||
<div className="system-workspace__actions">
|
||||
<StatusBadge tone={connected ? "success" : "danger"}>
|
||||
{connected ? "Узел доступен" : "Нет связи с узлом"}
|
||||
{connected
|
||||
? agentTelemetry ? "Агент доступен" : "SSH-диагностика"
|
||||
: "Нет свежих данных"}
|
||||
</StatusBadge>
|
||||
<Button
|
||||
size="compact"
|
||||
@@ -67,6 +81,13 @@ export function ComputeModulesWorkspace() {
|
||||
<StatusBadge tone="warning">{error}</StatusBadge>
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
{!legacyDiagnostic && !agentTelemetry ? (
|
||||
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
|
||||
<StatusBadge tone="warning">
|
||||
Агентный data-plane ещё не опубликовал нормализованный срез этого контура.
|
||||
</StatusBadge>
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
|
||||
<section className="system-telemetry-grid" aria-label="Аппаратная телеметрия">
|
||||
<TelemetrySeries
|
||||
@@ -99,11 +120,11 @@ export function ComputeModulesWorkspace() {
|
||||
<header className="system-section-heading">
|
||||
<div>
|
||||
<span className="section-eyebrow">HARDWARE</span>
|
||||
<h3>{node?.node_id ?? telemetry?.profile.expected_node_id ?? "Worker 006"}</h3>
|
||||
<h3>{node?.node_id ?? selectedContour?.expected_node_id ?? "Node ID не назначен"}</h3>
|
||||
<p>{node?.os.caption ?? "Аппаратный профиль недоступен"}</p>
|
||||
</div>
|
||||
<StatusBadge tone={node?.node_id ? "success" : "danger"}>
|
||||
{node?.node_id ? telemetry?.profile.display_name : "Нет данных"}
|
||||
{node?.node_id ? selectedContour?.display_name : "Нет данных"}
|
||||
</StatusBadge>
|
||||
</header>
|
||||
<div className="worker-hardware__facts">
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
GlassSurface,
|
||||
Icon,
|
||||
StatusBadge,
|
||||
TextField,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
import { TelemetrySeries } from "../../components/system/TelemetrySeries";
|
||||
@@ -14,118 +12,49 @@ import {
|
||||
formatLatency,
|
||||
formatRate,
|
||||
} from "../../components/system/systemFormat";
|
||||
import { useComputeContours } from "../../core/system/ComputeContourContext";
|
||||
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 подтверждён";
|
||||
}
|
||||
useWorkerTelemetry,
|
||||
WORKER_TELEMETRY_POLL_MILLISECONDS,
|
||||
} from "../../core/system/useWorkerTelemetry";
|
||||
|
||||
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 { selectedContour } = useComputeContours();
|
||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||
const { telemetry, loading, error, refresh } = useWorkerTelemetry(
|
||||
WORKER_TELEMETRY_POLL_MILLISECONDS,
|
||||
supportsLiveTelemetry,
|
||||
);
|
||||
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"));
|
||||
};
|
||||
const agentTelemetry = telemetry?.connection.source === "agent-mqtt";
|
||||
const runtimeOnline = telemetry?.runtimes.some(
|
||||
(runtime) => !runtime.external && runtime.state === "running",
|
||||
) ?? false;
|
||||
|
||||
return (
|
||||
<div className="system-workspace network-workspace">
|
||||
<section className="system-workspace__lead">
|
||||
<div>
|
||||
<span className="section-eyebrow">СИСТЕМА / СЕТЬ</span>
|
||||
<h2>Локальный вычислительный контур</h2>
|
||||
<h2>{selectedContour?.display_name ?? "Контур не выбран"}</h2>
|
||||
<p>
|
||||
Переносимый профиль связи между Mission Core и Worker 006. Адрес можно заменить
|
||||
при переходе в другую локальную сеть; идентичность узла проверяется до сохранения.
|
||||
Фактический локальный маршрут и сетевые счётчики выбранного вычислительного
|
||||
контура. Адрес, транспорт и установка агента находятся в настройках контура.
|
||||
</p>
|
||||
</div>
|
||||
<div className="system-workspace__actions">
|
||||
<StatusBadge tone={connected ? "success" : "danger"}>
|
||||
{connected ? "Маршрут доступен" : "Маршрут недоступен"}
|
||||
{connected ? "Маршрут доступен" : "Нет свежих данных"}
|
||||
</StatusBadge>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
disabled={loading}
|
||||
disabled={loading || !supportsLiveTelemetry}
|
||||
onClick={refresh}
|
||||
icon={<Icon name="refresh" size={14} />}
|
||||
>
|
||||
@@ -134,9 +63,16 @@ export function NetworkWorkspace() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{(error || profileError) ? (
|
||||
{error ? (
|
||||
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
|
||||
<StatusBadge tone="warning">{profileError ?? error}</StatusBadge>
|
||||
<StatusBadge tone="warning">{error}</StatusBadge>
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
{!legacyDiagnostic && !agentTelemetry ? (
|
||||
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
|
||||
<StatusBadge tone="warning">
|
||||
Ожидается первый нормализованный MQTT sample выбранного контура.
|
||||
</StatusBadge>
|
||||
</GlassSurface>
|
||||
) : null}
|
||||
|
||||
@@ -156,122 +92,52 @@ export function NetworkWorkspace() {
|
||||
)}
|
||||
/>
|
||||
<div className="network-stat-card">
|
||||
<span>Сбор телеметрии</span>
|
||||
<span>Источник данных</span>
|
||||
<strong>{formatLatency(telemetry?.connection.latency_ms)}</strong>
|
||||
<small>полный SSH probe Worker 006</small>
|
||||
<small>{agentTelemetry ? "agent → MQTT → normalizer" : "SSH diagnostic fallback"}</small>
|
||||
</div>
|
||||
<div className="network-stat-card">
|
||||
<span>Активные интерфейсы</span>
|
||||
<strong>{telemetry?.network.interfaces.length ?? "—"}</strong>
|
||||
<small>по Windows network counters</small>
|
||||
<small>{agentTelemetry ? "Telegraf net input" : "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="1–65535"
|
||||
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>
|
||||
<h3>Поток телеметрии и обработки</h3>
|
||||
<p>Схема отражает выбранный профиль; отсутствующие узлы не подменяются demo-состоянием.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="network-route">
|
||||
<div data-state="online">
|
||||
<span>Операторский UI</span>
|
||||
<strong>Browser</strong>
|
||||
<strong>Mission Core</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 data-state={agentTelemetry ? "online" : "offline"}>
|
||||
<span>Telemetry plane</span>
|
||||
<strong>{agentTelemetry ? "Mosquitto + Timescale" : "SSH diagnostic"}</strong>
|
||||
<small>
|
||||
{agentTelemetry
|
||||
? `${selectedContour?.mqtt_host ?? "—"}:${selectedContour?.mqtt_port ?? "—"}`
|
||||
: "временный диагностический путь"}
|
||||
</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>
|
||||
<span>Вычислительный контур</span>
|
||||
<strong>{selectedContour?.display_name ?? "Не выбран"}</strong>
|
||||
<small>{selectedContour?.address || selectedContour?.expected_node_id || "—"}</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"
|
||||
}>
|
||||
<i aria-hidden="true" data-state={runtimeOnline ? "online" : "offline"} />
|
||||
<div data-state={runtimeOnline ? "online" : "offline"}>
|
||||
<span>Runtime</span>
|
||||
<strong>Triton + Pipeline</strong>
|
||||
<small>внутренний Docker-контур</small>
|
||||
<strong>Inference + Pipeline</strong>
|
||||
<small>контейнеры выбранного узла</small>
|
||||
</div>
|
||||
</div>
|
||||
</GlassSurface>
|
||||
@@ -279,9 +145,9 @@ export function NetworkWorkspace() {
|
||||
<section className="network-interface-section">
|
||||
<header className="system-section-heading">
|
||||
<div>
|
||||
<span className="section-eyebrow">ИНТЕРФЕЙСЫ WORKER 006</span>
|
||||
<span className="section-eyebrow">ИНТЕРФЕЙСЫ УЗЛА</span>
|
||||
<h3>Адаптеры и счётчики</h3>
|
||||
<p>Только активные интерфейсы, которые вернул сам узел.</p>
|
||||
<p>Только фактически опубликованные интерфейсы выбранного контура.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="network-interface-list">
|
||||
|
||||
Reference in New Issue
Block a user