feat(system): add Worker 006 telemetry and network profile
This commit is contained in:
@@ -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 "Запущен";
|
||||
}
|
||||
Reference in New Issue
Block a user