feat(control-station): add compute contour workspace

This commit is contained in:
DCCONSTRUCTIONS
2026-07-28 00:54:49 +03:00
parent 1f4c960cc4
commit 1c0181297d
16 changed files with 987 additions and 262 deletions
@@ -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="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>
<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">