feat(system): configure telemetry polling per contour
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FeatureSettingsWindow,
|
FeatureSettingsWindow,
|
||||||
|
FieldFrame,
|
||||||
Icon,
|
Icon,
|
||||||
Select,
|
Select,
|
||||||
SettingsCard,
|
SettingsCard,
|
||||||
@@ -50,6 +51,7 @@ function emptyDraft(): ComputeContourDraft {
|
|||||||
ssh_port: 22,
|
ssh_port: 22,
|
||||||
mqtt_host: "127.0.0.1",
|
mqtt_host: "127.0.0.1",
|
||||||
mqtt_port: 1883,
|
mqtt_port: 1883,
|
||||||
|
telemetry_poll_interval_seconds: 3,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ function draftFromContour(contour: ComputeContour | null): ComputeContourDraft {
|
|||||||
ssh_port: contour.ssh_port,
|
ssh_port: contour.ssh_port,
|
||||||
mqtt_host: contour.mqtt_host,
|
mqtt_host: contour.mqtt_host,
|
||||||
mqtt_port: contour.mqtt_port,
|
mqtt_port: contour.mqtt_port,
|
||||||
|
telemetry_poll_interval_seconds: contour.telemetry_poll_interval_seconds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +114,9 @@ export function ComputeContourSettingsWindow({
|
|||||||
&& draft.ssh_port > 0
|
&& draft.ssh_port > 0
|
||||||
&& Number.isInteger(draft.mqtt_port)
|
&& Number.isInteger(draft.mqtt_port)
|
||||||
&& draft.mqtt_port > 0
|
&& draft.mqtt_port > 0
|
||||||
|
&& Number.isInteger(draft.telemetry_poll_interval_seconds)
|
||||||
|
&& draft.telemetry_poll_interval_seconds >= 1
|
||||||
|
&& draft.telemetry_poll_interval_seconds <= 60
|
||||||
), [draft]);
|
), [draft]);
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
@@ -194,14 +200,16 @@ export function ComputeContourSettingsWindow({
|
|||||||
expected_node_id: event.currentTarget.value,
|
expected_node_id: event.currentTarget.value,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<Select
|
<FieldFrame label="Операционная система">
|
||||||
label="Платформа"
|
<Select
|
||||||
value={draft.platform}
|
label="Операционная система"
|
||||||
options={platformOptions}
|
value={draft.platform}
|
||||||
variant="split"
|
options={platformOptions}
|
||||||
menuWidth="anchor"
|
variant="split"
|
||||||
onChange={(platform) => setDraft((current) => ({ ...current, platform }))}
|
menuWidth="anchor"
|
||||||
/>
|
onChange={(platform) => setDraft((current) => ({ ...current, platform }))}
|
||||||
|
/>
|
||||||
|
</FieldFrame>
|
||||||
<TextField
|
<TextField
|
||||||
label="Адрес узла"
|
label="Адрес узла"
|
||||||
hint="IP или hostname в текущей LAN"
|
hint="IP или hostname в текущей LAN"
|
||||||
@@ -230,6 +238,18 @@ export function ComputeContourSettingsWindow({
|
|||||||
mqtt_port: Number(event.currentTarget.value),
|
mqtt_port: Number(event.currentTarget.value),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Интервал MQTT"
|
||||||
|
hint="1–60 с"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={60}
|
||||||
|
value={String(draft.telemetry_poll_interval_seconds)}
|
||||||
|
onChange={(event) => setDraft((current) => ({
|
||||||
|
...current,
|
||||||
|
telemetry_poll_interval_seconds: Number(event.currentTarget.value),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
{draft.telemetry_mode === "legacy-ssh" ? (
|
{draft.telemetry_mode === "legacy-ssh" ? (
|
||||||
<TextField
|
<TextField
|
||||||
label="SSH port"
|
label="SSH port"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface ComputeContour {
|
|||||||
ssh_port: number;
|
ssh_port: number;
|
||||||
mqtt_host: string;
|
mqtt_host: string;
|
||||||
mqtt_port: number;
|
mqtt_port: number;
|
||||||
|
telemetry_poll_interval_seconds: number;
|
||||||
revision: number;
|
revision: number;
|
||||||
updated_at_utc: string | null;
|
updated_at_utc: string | null;
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,7 @@ export interface ComputeContourDraft {
|
|||||||
ssh_port: number;
|
ssh_port: number;
|
||||||
mqtt_host: string;
|
mqtt_host: string;
|
||||||
mqtt_port: number;
|
mqtt_port: number;
|
||||||
|
telemetry_poll_interval_seconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputeContourAgentInstall {
|
export interface ComputeContourAgentInstall {
|
||||||
@@ -96,6 +98,7 @@ export async function createComputeContour(
|
|||||||
ssh_port: draft.ssh_port,
|
ssh_port: draft.ssh_port,
|
||||||
mqtt_host: draft.mqtt_host,
|
mqtt_host: draft.mqtt_host,
|
||||||
mqtt_port: draft.mqtt_port,
|
mqtt_port: draft.mqtt_port,
|
||||||
|
telemetry_poll_interval_seconds: draft.telemetry_poll_interval_seconds,
|
||||||
}),
|
}),
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type WorkerTelemetry,
|
type WorkerTelemetry,
|
||||||
} from "./workerTelemetry";
|
} from "./workerTelemetry";
|
||||||
|
|
||||||
export const WORKER_TELEMETRY_POLL_MILLISECONDS = 3_000;
|
export const DEFAULT_WORKER_TELEMETRY_POLL_MILLISECONDS = 3_000;
|
||||||
|
|
||||||
export interface WorkerTelemetryState {
|
export interface WorkerTelemetryState {
|
||||||
telemetry: WorkerTelemetry | null;
|
telemetry: WorkerTelemetry | null;
|
||||||
@@ -15,7 +15,7 @@ export interface WorkerTelemetryState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkerTelemetry(
|
export function useWorkerTelemetry(
|
||||||
pollMilliseconds = WORKER_TELEMETRY_POLL_MILLISECONDS,
|
pollMilliseconds = DEFAULT_WORKER_TELEMETRY_POLL_MILLISECONDS,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
): WorkerTelemetryState {
|
): WorkerTelemetryState {
|
||||||
const [telemetry, setTelemetry] = useState<WorkerTelemetry | null>(null);
|
const [telemetry, setTelemetry] = useState<WorkerTelemetry | null>(null);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import { useComputeContours } from "../../core/system/ComputeContourContext";
|
import { useComputeContours } from "../../core/system/ComputeContourContext";
|
||||||
import {
|
import {
|
||||||
useWorkerTelemetry,
|
useWorkerTelemetry,
|
||||||
WORKER_TELEMETRY_POLL_MILLISECONDS,
|
|
||||||
} from "../../core/system/useWorkerTelemetry";
|
} from "../../core/system/useWorkerTelemetry";
|
||||||
|
|
||||||
function pipelineStateLabel(state: string): string {
|
function pipelineStateLabel(state: string): string {
|
||||||
@@ -30,8 +29,8 @@ export function ComputeModulesWorkspace() {
|
|||||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||||
const { telemetry, loading, error, refresh } = useWorkerTelemetry(
|
const { telemetry, error, refresh } = useWorkerTelemetry(
|
||||||
WORKER_TELEMETRY_POLL_MILLISECONDS,
|
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
||||||
supportsLiveTelemetry,
|
supportsLiveTelemetry,
|
||||||
);
|
);
|
||||||
const node = telemetry?.node ?? null;
|
const node = telemetry?.node ?? null;
|
||||||
@@ -67,11 +66,11 @@ export function ComputeModulesWorkspace() {
|
|||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={loading}
|
disabled={!supportsLiveTelemetry}
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
icon={<Icon name="refresh" size={14} />}
|
icon={<Icon name="refresh" size={14} />}
|
||||||
>
|
>
|
||||||
{loading ? "Читаем" : "Обновить"}
|
Обновить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
import { useComputeContours } from "../../core/system/ComputeContourContext";
|
import { useComputeContours } from "../../core/system/ComputeContourContext";
|
||||||
import {
|
import {
|
||||||
useWorkerTelemetry,
|
useWorkerTelemetry,
|
||||||
WORKER_TELEMETRY_POLL_MILLISECONDS,
|
|
||||||
} from "../../core/system/useWorkerTelemetry";
|
} from "../../core/system/useWorkerTelemetry";
|
||||||
|
|
||||||
export function NetworkWorkspace() {
|
export function NetworkWorkspace() {
|
||||||
@@ -23,8 +22,8 @@ export function NetworkWorkspace() {
|
|||||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||||
const { telemetry, loading, error, refresh } = useWorkerTelemetry(
|
const { telemetry, error, refresh } = useWorkerTelemetry(
|
||||||
WORKER_TELEMETRY_POLL_MILLISECONDS,
|
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
||||||
supportsLiveTelemetry,
|
supportsLiveTelemetry,
|
||||||
);
|
);
|
||||||
const aggregate = telemetry?.network.aggregate ?? null;
|
const aggregate = telemetry?.network.aggregate ?? null;
|
||||||
@@ -54,11 +53,11 @@ export function NetworkWorkspace() {
|
|||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={loading || !supportsLiveTelemetry}
|
disabled={!supportsLiveTelemetry}
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
icon={<Icon name="refresh" size={14} />}
|
icon={<Icon name="refresh" size={14} />}
|
||||||
>
|
>
|
||||||
{loading ? "Читаем" : "Обновить"}
|
Обновить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
|||||||
pipelineStages,
|
pipelineStages,
|
||||||
networkWorkspace,
|
networkWorkspace,
|
||||||
telemetryPolling,
|
telemetryPolling,
|
||||||
|
contourSettings,
|
||||||
|
contourContract,
|
||||||
telemetryStyles,
|
telemetryStyles,
|
||||||
styles,
|
styles,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
@@ -27,6 +29,8 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
|||||||
read("components/system/WorkerPipelineStages.tsx"),
|
read("components/system/WorkerPipelineStages.tsx"),
|
||||||
read("workspaces/system/NetworkWorkspace.tsx"),
|
read("workspaces/system/NetworkWorkspace.tsx"),
|
||||||
read("core/system/useWorkerTelemetry.ts"),
|
read("core/system/useWorkerTelemetry.ts"),
|
||||||
|
read("components/system/ComputeContourSettingsWindow.tsx"),
|
||||||
|
read("core/system/computeContours.ts"),
|
||||||
read("styles/system-telemetry.css"),
|
read("styles/system-telemetry.css"),
|
||||||
read("styles.css"),
|
read("styles.css"),
|
||||||
]);
|
]);
|
||||||
@@ -41,20 +45,27 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
|||||||
assert.match(core, /share_percent/);
|
assert.match(core, /share_percent/);
|
||||||
assert.doesNotMatch(core, /@nodedc\/ui-react/);
|
assert.doesNotMatch(core, /@nodedc\/ui-react/);
|
||||||
assert.match(computeWorkspace, /useWorkerTelemetry/);
|
assert.match(computeWorkspace, /useWorkerTelemetry/);
|
||||||
assert.match(computeWorkspace, /WORKER_TELEMETRY_POLL_MILLISECONDS/);
|
assert.match(computeWorkspace, /telemetry_poll_interval_seconds/);
|
||||||
|
assert.match(computeWorkspace, />\s*Обновить\s*</);
|
||||||
|
assert.doesNotMatch(computeWorkspace, /loading\s*\?\s*"Читаем"/);
|
||||||
assert.match(computeWorkspace, /WorkerPipelineStages/);
|
assert.match(computeWorkspace, /WorkerPipelineStages/);
|
||||||
assert.match(pipelineStages, /измеренного времени/);
|
assert.match(pipelineStages, /измеренного времени/);
|
||||||
assert.doesNotMatch(pipelineStages, /CPU|GPU|hardware/);
|
assert.doesNotMatch(pipelineStages, /CPU|GPU|hardware/);
|
||||||
assert.match(
|
assert.match(
|
||||||
telemetryPolling,
|
telemetryPolling,
|
||||||
/WORKER_TELEMETRY_POLL_MILLISECONDS\s*=\s*3_000/,
|
/DEFAULT_WORKER_TELEMETRY_POLL_MILLISECONDS\s*=\s*3_000/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
telemetryStyles,
|
telemetryStyles,
|
||||||
/\.worker-stage-list\s*\{[^}]*grid-template-columns:\s*repeat\(3,/s,
|
/\.worker-stage-list\s*\{[^}]*grid-template-columns:\s*repeat\(3,/s,
|
||||||
);
|
);
|
||||||
assert.match(networkWorkspace, /127\.0\.0\.1:8000/);
|
assert.match(networkWorkspace, /127\.0\.0\.1:8000/);
|
||||||
assert.match(networkWorkspace, /WORKER_TELEMETRY_POLL_MILLISECONDS/);
|
assert.match(networkWorkspace, /telemetry_poll_interval_seconds/);
|
||||||
|
assert.match(networkWorkspace, />\s*Обновить\s*</);
|
||||||
|
assert.doesNotMatch(networkWorkspace, /loading\s*\?\s*"Читаем"/);
|
||||||
assert.doesNotMatch(networkWorkspace, /8765/);
|
assert.doesNotMatch(networkWorkspace, /8765/);
|
||||||
|
assert.match(contourSettings, /FieldFrame label="Операционная система"/);
|
||||||
|
assert.match(contourSettings, /label="Интервал MQTT"/);
|
||||||
|
assert.match(contourContract, /telemetry_poll_interval_seconds: number/);
|
||||||
assert.match(styles, /system-telemetry\.css/);
|
assert.match(styles, /system-telemetry\.css/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class ComputeContour(StrictModel):
|
|||||||
ssh_port: int = Field(default=22, ge=1, le=65535)
|
ssh_port: int = Field(default=22, ge=1, le=65535)
|
||||||
mqtt_host: str = Field(default="127.0.0.1", min_length=1, max_length=253)
|
mqtt_host: str = Field(default="127.0.0.1", min_length=1, max_length=253)
|
||||||
mqtt_port: int = Field(default=1883, ge=1, le=65535)
|
mqtt_port: int = Field(default=1883, ge=1, le=65535)
|
||||||
|
telemetry_poll_interval_seconds: int = Field(default=3, ge=1, le=60)
|
||||||
revision: int = Field(default=0, ge=0)
|
revision: int = Field(default=0, ge=0)
|
||||||
updated_at_utc: str | None = None
|
updated_at_utc: str | None = None
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ class ComputeContourCreate(StrictModel):
|
|||||||
ssh_port: int = Field(default=22, ge=1, le=65535)
|
ssh_port: int = Field(default=22, ge=1, le=65535)
|
||||||
mqtt_host: str = Field(default="127.0.0.1", min_length=1, max_length=253)
|
mqtt_host: str = Field(default="127.0.0.1", min_length=1, max_length=253)
|
||||||
mqtt_port: int = Field(default=1883, ge=1, le=65535)
|
mqtt_port: int = Field(default=1883, ge=1, le=65535)
|
||||||
|
telemetry_poll_interval_seconds: int = Field(default=3, ge=1, le=60)
|
||||||
|
|
||||||
@field_validator("display_name")
|
@field_validator("display_name")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -124,6 +126,7 @@ def default_compute_contour() -> ComputeContour:
|
|||||||
ssh_port=22,
|
ssh_port=22,
|
||||||
mqtt_host="127.0.0.1",
|
mqtt_host="127.0.0.1",
|
||||||
mqtt_port=1883,
|
mqtt_port=1883,
|
||||||
|
telemetry_poll_interval_seconds=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +155,9 @@ class ComputeContourStore:
|
|||||||
ssh_port=request.ssh_port,
|
ssh_port=request.ssh_port,
|
||||||
mqtt_host=request.mqtt_host,
|
mqtt_host=request.mqtt_host,
|
||||||
mqtt_port=request.mqtt_port,
|
mqtt_port=request.mqtt_port,
|
||||||
|
telemetry_poll_interval_seconds=(
|
||||||
|
request.telemetry_poll_interval_seconds
|
||||||
|
),
|
||||||
revision=0,
|
revision=0,
|
||||||
updated_at_utc=_utc_now(),
|
updated_at_utc=_utc_now(),
|
||||||
)
|
)
|
||||||
@@ -177,6 +183,9 @@ class ComputeContourStore:
|
|||||||
"ssh_port": request.ssh_port,
|
"ssh_port": request.ssh_port,
|
||||||
"mqtt_host": request.mqtt_host,
|
"mqtt_host": request.mqtt_host,
|
||||||
"mqtt_port": request.mqtt_port,
|
"mqtt_port": request.mqtt_port,
|
||||||
|
"telemetry_poll_interval_seconds": (
|
||||||
|
request.telemetry_poll_interval_seconds
|
||||||
|
),
|
||||||
"revision": current.revision + 1,
|
"revision": current.revision + 1,
|
||||||
"updated_at_utc": _utc_now(),
|
"updated_at_utc": _utc_now(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ def test_contour_store_migrates_worker_006_as_first_configuration(
|
|||||||
assert len(contours) == 1
|
assert len(contours) == 1
|
||||||
assert contours[0].contour_id == "worker-006"
|
assert contours[0].contour_id == "worker-006"
|
||||||
assert contours[0].telemetry_mode == "agent-mqtt"
|
assert contours[0].telemetry_mode == "agent-mqtt"
|
||||||
|
assert contours[0].telemetry_poll_interval_seconds == 3
|
||||||
assert not store.path.exists()
|
assert not store.path.exists()
|
||||||
|
|
||||||
|
|
||||||
@@ -65,10 +66,12 @@ def test_contour_store_creates_and_updates_private_catalog(tmp_path: Path) -> No
|
|||||||
ssh_port=22,
|
ssh_port=22,
|
||||||
mqtt_host="192.0.2.5",
|
mqtt_host="192.0.2.5",
|
||||||
mqtt_port=1883,
|
mqtt_port=1883,
|
||||||
|
telemetry_poll_interval_seconds=1,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert updated.display_name == "Field Worker 01"
|
assert updated.display_name == "Field Worker 01"
|
||||||
|
assert updated.telemetry_poll_interval_seconds == 1
|
||||||
assert updated.revision == 1
|
assert updated.revision == 1
|
||||||
assert stat.S_IMODE(store.path.stat().st_mode) == 0o600
|
assert stat.S_IMODE(store.path.stat().st_mode) == 0o600
|
||||||
assert len(store.list_contours()) == 2
|
assert len(store.list_contours()) == 2
|
||||||
@@ -85,6 +88,7 @@ def test_contour_store_creates_and_updates_private_catalog(tmp_path: Path) -> No
|
|||||||
ssh_port=22,
|
ssh_port=22,
|
||||||
mqtt_host="127.0.0.1",
|
mqtt_host="127.0.0.1",
|
||||||
mqtt_port=1883,
|
mqtt_port=1883,
|
||||||
|
telemetry_poll_interval_seconds=3,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user