fix(ui): align telemetry and laboratory navigation

This commit is contained in:
DCCONSTRUCTIONS
2026-07-28 03:18:27 +03:00
parent 6db5ac05ef
commit e35f81ac34
13 changed files with 195 additions and 55 deletions
@@ -1,6 +1,7 @@
import {
useEffect,
useMemo,
useRef,
useState,
type ComponentType,
} from "react";
@@ -71,6 +72,11 @@ const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
e37: null,
};
function laboratoryWorkOrdinal(value: string): number {
const match = value.match(/\bE(\d+)\b/i);
return match ? Number(match[1]) : -1;
}
function digestFromContentId(value: string | null | undefined): string | null {
const digest = value?.split("-").at(-1) ?? "";
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
@@ -559,6 +565,7 @@ function PublishedLaboratoryResult({
export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const [profileId, setProfileId] = useState<LaboratoryProfileId>("sensor-fusion");
const [workId, setWorkId] = useState<LaboratoryWorkId>("e28-local-surface");
const initialWorkSelectedRef = useRef(false);
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
@@ -580,7 +587,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
&& session.status === "ready"
&& session.replayable
&& session.modalities.includes("point-cloud")
)),
)).sort((left, right) => {
const ordinalDelta = laboratoryWorkOrdinal(right.lab?.labId ?? "")
- laboratoryWorkOrdinal(left.lab?.labId ?? "");
if (ordinalDelta !== 0) return ordinalDelta;
return (right.startedAtUtc ?? "").localeCompare(left.startedAtUtc ?? "");
}),
[sessions.items],
);
const sourceSessions = useMemo(
@@ -661,7 +673,9 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
advancedResults,
sourceSessions,
));
return items;
return items.sort(
(left, right) => laboratoryWorkOrdinal(right.label) - laboratoryWorkOrdinal(left.label),
);
}, [
advancedResults,
e28Model,
@@ -717,13 +731,26 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
setProfileId(firstProfile.id);
if (firstProfile.id === "sensor-fusion") {
const firstWork = sensorWorks[0];
if (firstWork) setWorkId(firstWork.id);
if (firstWork) {
setWorkId(firstWork.id);
initialWorkSelectedRef.current = true;
}
} else {
const first = publishedWorks[0];
if (first) setWorkId(`session:${first.id}`);
if (first) {
setWorkId(`session:${first.id}`);
initialWorkSelectedRef.current = true;
}
}
return;
}
if (!initialWorkSelectedRef.current) {
const freshestWork = workOptions[0];
if (!freshestWork) return;
setWorkId(freshestWork.id);
initialWorkSelectedRef.current = true;
return;
}
if (!workOptions.some((work) => work.id === workId)) {
const firstWork = workOptions[0];
if (firstWork) setWorkId(firstWork.id);
@@ -741,6 +768,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const selectProfile = (next: LaboratoryProfileId) => {
setProfileId(next);
initialWorkSelectedRef.current = true;
if (next === "sensor-fusion") {
const first = sensorWorks[0];
if (first) setWorkId(first.id);
@@ -1,7 +1,5 @@
import {
Button,
GlassSurface,
Icon,
StatusBadge,
} from "@nodedc/ui-react";
@@ -24,14 +22,53 @@ function pipelineStateLabel(state: string): string {
return "Нет live-состояния";
}
function formatResourcePair(
used: number | null | undefined,
total: number | null | undefined,
): string | null {
if (
typeof used !== "number"
|| !Number.isFinite(used)
|| typeof total !== "number"
|| !Number.isFinite(total)
|| total <= 0
) {
return null;
}
return `${formatBytes(used)} / ${formatBytes(total)}`;
}
function formatCpuCapacity(
percent: number | null | undefined,
logicalProcessors: number | null | undefined,
): string | null {
if (
typeof percent !== "number"
|| !Number.isFinite(percent)
|| typeof logicalProcessors !== "number"
|| !Number.isFinite(logicalProcessors)
|| logicalProcessors <= 0
) {
return null;
}
const used = new Intl.NumberFormat("ru-RU", {
maximumFractionDigits: 1,
}).format(percent / 100 * logicalProcessors);
return `${used} / ${logicalProcessors} лог. ядра`;
}
export function ComputeModulesWorkspace() {
const { selectedContour } = useComputeContours();
const {
selectedContour,
telemetryRefreshGeneration,
} = useComputeContours();
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
&& selectedContour.telemetry_mode === "legacy-ssh";
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
const { telemetry, error, refresh } = useWorkerTelemetry(
const { telemetry, error } = useWorkerTelemetry(
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
supportsLiveTelemetry,
telemetryRefreshGeneration,
);
const node = telemetry?.node ?? null;
const missionCoreRuntimes = telemetry?.runtimes.filter((runtime) => !runtime.external) ?? [];
@@ -45,6 +82,12 @@ export function ComputeModulesWorkspace() {
const memoryPercent = node?.memory.used_percent;
const gpuPercent = node?.gpu?.utilization_percent;
const gpuMemoryPercent = node?.gpu?.memory_used_percent;
const gpuMemoryUsedBytes = typeof node?.gpu?.memory_used_mib === "number"
? node.gpu.memory_used_mib * 1024 * 1024
: null;
const gpuMemoryTotalBytes = typeof node?.gpu?.memory_total_mib === "number"
? node.gpu.memory_total_mib * 1024 * 1024
: null;
return (
<div className="system-workspace compute-modules-workspace">
@@ -63,15 +106,6 @@ export function ComputeModulesWorkspace() {
? agentTelemetry ? "Агент доступен" : "SSH-диагностика"
: "Нет свежих данных"}
</StatusBadge>
<Button
size="compact"
variant="secondary"
disabled={!supportsLiveTelemetry}
onClick={refresh}
icon={<Icon name="refresh" size={14} />}
>
Обновить
</Button>
</div>
</section>
@@ -92,24 +126,37 @@ export function ComputeModulesWorkspace() {
<TelemetrySeries
label="CPU"
value={formatOptionalPercent(cpuPercent)}
resource={formatCpuCapacity(
cpuPercent,
node?.cpu.logical_processors,
)}
ceiling={100}
values={history.map((item) => item.cpu_percent)}
/>
<TelemetrySeries
label="RAM"
value={formatOptionalPercent(memoryPercent)}
resource={formatResourcePair(
node?.memory.used_bytes,
node?.memory.total_bytes,
)}
ceiling={100}
values={history.map((item) => item.memory_percent)}
/>
<TelemetrySeries
label="GPU"
value={formatOptionalPercent(gpuPercent)}
resource={node?.gpu?.name ?? null}
ceiling={100}
values={history.map((item) => item.gpu_percent)}
/>
<TelemetrySeries
label="VRAM"
value={formatOptionalPercent(gpuMemoryPercent)}
resource={formatResourcePair(
gpuMemoryUsedBytes,
gpuMemoryTotalBytes,
)}
ceiling={100}
values={history.map((item) => item.gpu_memory_percent)}
/>
@@ -135,11 +182,7 @@ export function ComputeModulesWorkspace() {
</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>VRAM занята</dt><dd>{formatBytes(gpuMemoryUsedBytes)}</dd></div>
<div><dt>Температура</dt><dd>{
typeof node?.gpu?.temperature_celsius === "number"
? `${node.gpu.temperature_celsius} °C`
@@ -1,7 +1,5 @@
import {
Button,
GlassSurface,
Icon,
StatusBadge,
} from "@nodedc/ui-react";
@@ -18,13 +16,17 @@ import {
} from "../../core/system/useWorkerTelemetry";
export function NetworkWorkspace() {
const { selectedContour } = useComputeContours();
const {
selectedContour,
telemetryRefreshGeneration,
} = useComputeContours();
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
&& selectedContour.telemetry_mode === "legacy-ssh";
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
const { telemetry, error, refresh } = useWorkerTelemetry(
const { telemetry, error } = useWorkerTelemetry(
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
supportsLiveTelemetry,
telemetryRefreshGeneration,
);
const aggregate = telemetry?.network.aggregate ?? null;
const connected = Boolean(
@@ -50,15 +52,6 @@ export function NetworkWorkspace() {
<StatusBadge tone={connected ? "success" : "danger"}>
{connected ? "Маршрут доступен" : "Нет свежих данных"}
</StatusBadge>
<Button
size="compact"
variant="secondary"
disabled={!supportsLiveTelemetry}
onClick={refresh}
icon={<Icon name="refresh" size={14} />}
>
Обновить
</Button>
</div>
</section>