import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from "react"; import { Button, GlassSurface, Icon, IconButton, Select, SettingsCard, StatusBadge, TextAreaField, TextField, Window, WindowFooterActions, } from "@nodedc/ui-react"; import { createConfigurationRevision, closeAssetBinding, ensureAsset, ensureAssetBinding, ensureAdapterPackage, ensureDeviceBinding, ensureEdge, ensureInfrastructureDeployment, ensureInfrastructureEndpoint, ensureInfrastructureHost, ensureInfrastructureServiceInstance, ensureRoute, registerAdapterVersion, registerModelProfile, recordHealthObservation, revokeDeviceBinding, sendServicePing, setDesiredConfiguration, upsertProjectGrant, } from "./api"; import type { AdapterPackageView, AdapterVersionView, AssetBindingView, BindingView, DeviceManagerSession, EdgeView, InfrastructureHostView, InfrastructureServiceInstanceView, ModelProfileView, ProjectWorkspace, } from "./types"; export type ControlViewId = | "catalog" | "infrastructure" | "hosts" | "sessions" | "bindings" | "commands" | "audit" | "access" | "settings"; type DialogId = | "adapter-package" | "adapter-version" | "model-profile" | "edge" | "route" | "binding" | "grant" | "configuration" | "asset" | "asset-binding" | "host" | "endpoint" | "deployment" | "service-instance" | "health-observation" | null; export function DeviceControlView({ view, workspace, session, onRefresh, onPoll, onError, }: { view: ControlViewId; workspace: ProjectWorkspace; session: DeviceManagerSession; onRefresh: () => Promise; onPoll: () => Promise; onError: (reason: unknown) => void; }) { const [dialog, setDialog] = useState(null); const capabilities = new Set(workspace.project.access.capabilities); const platformOwner = session.actor.hubRole === "owner"; const close = () => setDialog(null); const completed = async () => { close(); await onRefresh(); }; const mutateAndRefresh = async (mutation: () => Promise) => { try { await mutation(); await onRefresh(); } catch (reason) { onError(reason); } }; return ( <> {view === "catalog" ? ( setDialog("adapter-package")} onCreateVersion={() => setDialog("adapter-version")} onCreateProfile={() => setDialog("model-profile")} onActivateVersion={(version) => mutateAndRefresh(() => registerAdapterVersion({ adapterPackageRef: version.adapterPackageRef, version: version.version, runtimePackageRef: version.runtimePackageRef, contentDigest: version.contentDigest, contractVersion: version.contractVersion, capabilities: version.capabilities, lifecycleState: "active", }))} onActivateProfile={(profile) => mutateAndRefresh(() => registerModelProfile({ adapterVersionRef: profile.adapterVersionRef || "", profileRef: profile.modelProfileRef, schemaVersion: profile.schemaVersion, vendor: profile.vendor, model: profile.model, deviceType: profile.deviceType, protocol: profile.protocol, schemaArtifactRef: profile.schemaArtifactRef || "", profileDigest: profile.profileDigest || "", capabilities: profile.capabilities, lifecycleState: "active", }))} /> ) : null} {view === "infrastructure" ? ( setDialog("edge")} onCreateRoute={() => setDialog("route")} onActivateEdge={(edge) => mutateAndRefresh(() => ensureEdge({ edgeKey: edge.edgeKey, displayName: edge.displayName, deploymentRef: edge.deploymentRef, lifecycleState: "active", }))} onActivateRoute={(route) => mutateAndRefresh(() => ensureRoute({ projectRef: workspace.project.projectRef, routeKey: route.routeKey, displayName: route.displayName, edgeRef: route.edgeRef, modelProfileRef: route.modelProfileRef, listenerRef: route.listenerRef, protocol: route.protocol, direction: route.direction, lifecycleState: "active", }))} /> ) : null} {view === "hosts" ? ( setDialog("host")} onCreateEndpoint={() => setDialog("endpoint")} onCreateDeployment={() => setDialog("deployment")} onCreateService={() => setDialog("service-instance")} onRecordHealth={() => setDialog("health-observation")} onCreateAsset={() => setDialog("asset")} onCreateAssetBinding={() => setDialog("asset-binding")} onCloseAssetBinding={(binding) => mutateAndRefresh(() => closeAssetBinding({ projectRef: workspace.project.projectRef, assetBindingRef: binding.assetBindingRef, validTo: new Date().toISOString(), }))} onPoll={onPoll} onError={onError} /> ) : null} {view === "sessions" ? : null} {view === "bindings" ? ( setDialog("binding")} onRevoke={(binding) => revokeDeviceBinding({ projectRef: workspace.project.projectRef, bindingRef: binding.bindingRef, resolutionCode: "operator.revoked", }).then(onRefresh).catch(onError)} /> ) : null} {view === "commands" ? ( ) : null} {view === "audit" ? : null} {view === "access" ? ( setDialog("grant")} /> ) : null} {view === "settings" ? ( setDialog("configuration")} /> ) : null} ); } function CatalogView({ workspace, canManage, onCreatePackage, onCreateVersion, onCreateProfile, onActivateVersion, onActivateProfile }: { workspace: ProjectWorkspace; canManage: boolean; onCreatePackage: () => void; onCreateVersion: () => void; onCreateProfile: () => void; onActivateVersion: (version: AdapterVersionView) => void; onActivateProfile: (profile: ModelProfileView) => void; }) { return ( : null} /> {workspace.modelProfiles.map((profile) => ( onActivateProfile(profile)}>Активировать ) : null} /> ))} {workspace.adapterVersions.map((version) => ( onActivateVersion(version)}>Активировать ) : null} /> ))} {workspace.adapterPackages.map((adapterPackage) => ( version.adapterPackageRef === adapterPackage.adapterPackageRef) .map((version) => `${version.version} · ${version.lifecycleState}`)} /> ))} ); } function InfrastructureView({ workspace, canManageCatalog, canManageRoutes, onCreateEdge, onCreateRoute, onActivateEdge, onActivateRoute }: { workspace: ProjectWorkspace; canManageCatalog: boolean; canManageRoutes: boolean; onCreateEdge: () => void; onCreateRoute: () => void; onActivateEdge: (edge: EdgeView) => void; onActivateRoute: (route: ProjectWorkspace["routes"][number]) => void; }) { return ( {canManageCatalog ? : null} {canManageRoutes ? : null} } /> {workspace.routes.map((route) => ( onActivateRoute(route)}>Активировать ) : null} /> ))} {workspace.edges.map((edge) => ( onActivateEdge(edge)}>Активировать ) : null} /> ))} ); } function HostsView({ workspace, canManageInfrastructure, canManageAssets, canManageBindings, onCreateHost, onCreateEndpoint, onCreateDeployment, onCreateService, onRecordHealth, onCreateAsset, onCreateAssetBinding, onCloseAssetBinding, onPoll, onError, }: { workspace: ProjectWorkspace; canManageInfrastructure: boolean; canManageAssets: boolean; canManageBindings: boolean; onCreateHost: () => void; onCreateEndpoint: () => void; onCreateDeployment: () => void; onCreateService: () => void; onRecordHealth: () => void; onCreateAsset: () => void; onCreateAssetBinding: () => void; onCloseAssetBinding: (binding: AssetBindingView) => void; onPoll: () => Promise; onError: (reason: unknown) => void; }) { const [selectedHostRef, setSelectedHostRef] = useState(null); const topology = workspace.ontology; const selectedHost = selectedHostRef ? topology.hosts.find((host) => host.hostRef === selectedHostRef) ?? null : null; if (selectedHost) { return ( service.hostRef === selectedHost.hostRef)} onBack={() => setSelectedHostRef(null)} onPoll={onPoll} onError={onError} /> ); } return ( : null} /> {topology.hosts.map((host) => { const hostEndpoints = topology.endpoints.filter((item) => item.hostRef === host.hostRef); const hostServices = topology.serviceInstances.filter((item) => item.hostRef === host.hostRef); return ( {canManageInfrastructure ? ( ) : null} } /> ); })} {topology.serviceInstances.map((service) => { const edge = service.edgeRef ? workspace.edges.find((item) => item.edgeRef === service.edgeRef) : null; return ( ); })} {topology.deployments.map((deployment) => ( ))} {topology.endpoints.map((endpoint) => ( ))} {canManageAssets ? : null} {canManageBindings ? : null} } /> {topology.assets.map((asset) => { const activeBindings = topology.assetBindings.filter( (binding) => binding.assetRef === asset.assetRef && !binding.validTo, ); return ( ); })} {topology.assetBindings.map((binding) => ( onCloseAssetBinding(binding)}>Закрыть ) : formatDate(binding.validTo)} /> ))}

Отсутствующее или просроченное health evidence отображается как unobserved, а не unreachable. Arbitrary WebSSH console отключена; будущая консоль потребует отдельной короткоживущей management session и break-glass аудита.

); } function HostTelemetryWorkspace({ host, services, onBack, onPoll, onError, }: { host: InfrastructureHostView; services: InfrastructureServiceInstanceView[]; onBack: () => void; onPoll: () => Promise; onError: (reason: unknown) => void; }) { useEffect(() => { let active = true; const timer = window.setInterval(() => { onPoll().catch((reason) => active && onError(reason)); }, 3_000); return () => { active = false; window.clearInterval(timer); }; }, [onError, onPoll]); const telemetry = host.telemetry; const current = telemetry.current; const networkRate = calculateNetworkRate(telemetry.history); const cpuHistory = telemetry.history.map((sample) => sample.cpuUsagePercent); const memoryHistory = telemetry.history.map((sample) => sample.memoryUsedPercent); const receiveHistory = calculateNetworkRateHistory(telemetry.history, "received"); const sendHistory = calculateNetworkRateHistory(telemetry.history, "sent"); const runtimeServices = current?.services ?? []; return (
СИСТЕМА / ВЫЧИСЛИТЕЛЬНЫЕ МОДУЛИ

{host.displayName}

Аппаратный и процессинговый срез VPS. Метрики снимает host-agent; Device Core хранит только канонические наблюдения.

{telemetry.freshness === "fresh" ? "Свежие данные" : telemetry.freshness === "stale" ? "Данные устарели" : "Нет данных"} onPoll().catch(onError)}>
HARDWARE

{current?.hardware.hostname ?? host.hostKey}

{telemetry.state}
STORAGE

Файловые системы

{current?.disks.length ?? 0}
{(current?.disks ?? []).map((disk, index) => (
{disk.mount ?? disk.device ?? "Диск"}{[disk.device, disk.filesystem].filter(Boolean).join(" · ")} {formatPercent(disk.usedPercent)}{formatUsedTotal(disk.usedBytes, disk.totalBytes)}
))} {!current?.disks.length ?
Данные о дисках ещё не поступили.
: null}
NETWORK

Сетевые интерфейсы

{current?.network.length ?? 0}
{(current?.network ?? []).filter((item) => item.interface !== "lo").map((item, index) => (
{item.interface ?? "Интерфейс"}{formatPackets(item.packetsReceived, item.packetsSent)} ↓ {formatMetricBytes(item.bytesReceived)}↑ {formatMetricBytes(item.bytesSent)}
))} {!current?.network.filter((item) => item.interface !== "lo").length ?
Сетевые счётчики ещё не поступили.
: null}
PROCESSING RUNTIME

Сервисы VPS

Состояние systemd-юнитов и их ресурсный профиль.

service.activeState === "failed") ? "danger" : "success"}>{runtimeServices.length} units
{runtimeServices.map((service, index) => (
{service.name ?? "systemd unit"}{service.subState ?? service.loadState ?? "—"} {service.activeState ?? "unknown"}{formatMetricBytes(service.memoryBytes)}
))} {!runtimeServices.length ?
Состояние сервисов ещё не поступило.
: null}
ONTOLOGY / OBSERVATION{telemetry.observation?.entityId ?? "observation.observation"}
Источник{current ? `${current.source.agent} ${current.source.agentVersion}` : "—"}
Последнее наблюдение{formatDate(telemetry.observedAt)}
Связанные сервисы{services.length}
); } function TelemetryMetricCard({ label, value, points, detail }: { label: string; value: string; points: Array; detail: string }) { return (
{label} {value} {detail}
); } function Sparkline({ values }: { values: Array }) { const normalized = values.filter((value): value is number => typeof value === "number" && Number.isFinite(value)); if (normalized.length < 2) return
; const minimum = Math.min(...normalized); const maximum = Math.max(...normalized); const spread = Math.max(1, maximum - minimum); const points = normalized.map((value, index) => { const x = (index / (normalized.length - 1)) * 100; const y = 28 - ((value - minimum) / spread) * 24; return `${x.toFixed(2)},${y.toFixed(2)}`; }).join(" "); return ; } function TelemetryFact({ label, value }: { label: string; value: string | null | undefined }) { return
{label}{value || "—"}
; } function calculateNetworkRate(history: InfrastructureHostView["telemetry"]["history"]) { if (history.length < 2) return { received: null, sent: null }; const previous = history[history.length - 2]; const latest = history[history.length - 1]; const seconds = (new Date(latest.observedAt).valueOf() - new Date(previous.observedAt).valueOf()) / 1000; if (!Number.isFinite(seconds) || seconds <= 0) return { received: null, sent: null }; const previousTotals = networkTotals(previous.network); const latestTotals = networkTotals(latest.network); return { received: nonNegativeRate(latestTotals.received - previousTotals.received, seconds), sent: nonNegativeRate(latestTotals.sent - previousTotals.sent, seconds), }; } function calculateNetworkRateHistory(history: InfrastructureHostView["telemetry"]["history"], direction: "received" | "sent") { return history.slice(1).map((sample, index) => { const previous = history[index]; const seconds = (new Date(sample.observedAt).valueOf() - new Date(previous.observedAt).valueOf()) / 1000; if (seconds <= 0) return null; const currentTotals = networkTotals(sample.network); const previousTotals = networkTotals(previous.network); return nonNegativeRate(currentTotals[direction] - previousTotals[direction], seconds); }); } function networkTotals(network: InfrastructureHostView["telemetry"]["history"][number]["network"]) { return network.filter((item) => item.interface !== "lo").reduce((total, item) => ({ received: total.received + (item.bytesReceived ?? 0), sent: total.sent + (item.bytesSent ?? 0), }), { received: 0, sent: 0 }); } function nonNegativeRate(bytes: number, seconds: number) { const value = bytes / seconds; return Number.isFinite(value) && value >= 0 ? value : null; } function formatPercent(value: number | null | undefined) { return value == null ? "—" : `${value.toFixed(value >= 10 ? 0 : 1)}%`; } function formatRate(value: number | null) { return value == null ? "—" : `${formatMetricBytes(value)}/s`; } function formatMetricBytes(value: number | null | undefined) { if (value == null || !Number.isFinite(value)) return "—"; if (value < 1024) return `${Math.round(value)} B`; if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KiB`; if (value < 1024 ** 3) return `${(value / 1024 ** 2).toFixed(1)} MiB`; return `${(value / 1024 ** 3).toFixed(1)} GiB`; } function formatUsedTotal(used: number | null | undefined, total: number | null | undefined) { return used == null || total == null ? "—" : `${formatMetricBytes(used)} / ${formatMetricBytes(total)}`; } function formatNullable(value: number | null | undefined) { return value == null ? "—" : new Intl.NumberFormat("ru-RU").format(value); } function formatDuration(seconds: number | null | undefined) { if (seconds == null) return "—"; const days = Math.floor(seconds / 86_400); const hours = Math.floor((seconds % 86_400) / 3_600); const minutes = Math.floor((seconds % 3_600) / 60); return [days ? `${days} д` : null, hours ? `${hours} ч` : null, `${minutes} мин`].filter(Boolean).join(" "); } function formatLoad(cpu: { load1: number | null; load5: number | null; load15: number | null } | null | undefined) { if (!cpu || cpu.load1 == null) return "load average —"; return `load ${[cpu.load1, cpu.load5, cpu.load15].map((value) => value?.toFixed(2) ?? "—").join(" / ")}`; } function formatPackets(received: number | null | undefined, sent: number | null | undefined) { return `↓ ${formatNullable(received)} пакетов · ↑ ${formatNullable(sent)} пакетов`; } function SessionsView({ workspace }: { workspace: ProjectWorkspace }) { return ( {workspace.sessions.map((session) => ( ))} ); } function BindingsView({ workspace, canManage, onCreate, onRevoke }: { workspace: ProjectWorkspace; canManage: boolean; onCreate: () => void; onRevoke: (binding: BindingView) => void; }) { return ( Новый binding : null} /> {workspace.bindings.map((binding) => ( onRevoke(binding)}>Отозвать ) : binding.capabilities.join(", ")} /> ))} ); } function CommandsView({ workspace, canDispatch, onRefresh, onError }: { workspace: ProjectWorkspace; canDispatch: boolean; onRefresh: () => Promise; onError: (reason: unknown) => void; }) { const supportedDevices = workspace.devices.filter( (device) => device.modelProfileRef === "arusnavi.b2.internal.v1" && !["suspended", "retired"].includes(device.lifecycleState), ); const [deviceRef, setDeviceRef] = useState(supportedDevices[0]?.deviceRef ?? ""); const [accessCode, setAccessCode] = useState(""); const [submitting, setSubmitting] = useState(false); const enabled = workspace.policies.commandTransport === "typed-service-ping-v1"; useEffect(() => { if (!supportedDevices.some((device) => device.deviceRef === deviceRef)) { setDeviceRef(supportedDevices[0]?.deviceRef ?? ""); } }, [deviceRef, supportedDevices]); const submit = async (event: FormEvent) => { event.preventDefault(); if (!enabled || !canDispatch || !deviceRef || !/^\d{6}$/.test(accessCode)) return; setSubmitting(true); try { await sendServicePing({ projectRef: workspace.project.projectRef, deviceRef, accessCode, expiresInSeconds: 300, }); setAccessCode(""); await onRefresh(); } catch (reason) { onError(reason); } finally { setSubmitting(false); } }; return (
{enabled ? "Типизированный командный канал активен" : "Command transport выключен"}

{enabled ? "Доступна только безопасная проверка сервиса. Произвольные команды, прошивка, очистка памяти и перезагрузка отсутствуют. Код устройства существует только в памяти Core до отправки или истечения TTL." : "Ни UI, ни BFF не имеют raw command builder. acknowledged означает подтверждение протокола, verified — отдельное доказательство состояния."}

{workspace.policies.commandTransport}
{enabled ? (
({ value: item.adapterPackageRef, label: item.displayName }))} /> setVersion(event.target.value)} required placeholder="1.0.0" /> setRuntimeRef(event.target.value)} required /> setDigest(event.target.value)} required placeholder="sha256:…" /> setContractVersion(event.target.value)} required /> setCapabilities(event.target.value)} description="Через запятую" /> ; } function ModelProfileDialog({ versions, ...props }: DialogBaseProps & { versions: AdapterVersionView[] }) { const [versionRef, setVersionRef] = useState(versions[0]?.adapterVersionRef ?? ""); const [profileRef, setProfileRef] = useState(""); const [schemaVersion, setSchemaVersion] = useState(""); const [vendor, setVendor] = useState(""); const [model, setModel] = useState(""); const [deviceType, setDeviceType] = useState(""); const [protocol, setProtocol] = useState(""); const [schemaRef, setSchemaRef] = useState(""); const [digest, setDigest] = useState(""); const [capabilities, setCapabilities] = useState(""); useEffect(() => { if (!versions.some((item) => item.adapterVersionRef === versionRef)) { setVersionRef(versions[0]?.adapterVersionRef ?? ""); } }, [versionRef, versions]); return { await registerModelProfile({ adapterVersionRef: versionRef, profileRef, schemaVersion, vendor, model, deviceType, protocol: protocol.toUpperCase(), schemaArtifactRef: schemaRef, profileDigest: digest, capabilities: commaList(capabilities), lifecycleState: "draft", }); }}> ({ value: item.deviceRef, label: item.displayName, description: item.modelProfileRef }))} /> setProvenanceRef(event.target.value)} required /> ; } function HostDialog({ projectRef, ...props }: DialogBaseProps & { projectRef: string }) { const [hostKey, setHostKey] = useState(""); const [displayName, setDisplayName] = useState(""); const [providerRef, setProviderRef] = useState(""); const [externalRef, setExternalRef] = useState(""); const [credentialRef, setCredentialRef] = useState(""); return { await ensureInfrastructureHost({ projectRef, hostKey, displayName, providerRef: providerRef || null, externalRef: externalRef || null, managementCredentialRef: credentialRef || null, lifecycleState: "active", }); }}> setDisplayName(event.target.value)} required /> setProviderRef(event.target.value)} placeholder="provider:beget" /> setExternalRef(event.target.value)} placeholder="provider-resource:vps-123" /> setCredentialRef(event.target.value)} placeholder="secret-ref:device-core/host-key" description="Только server-side secret reference. Пароль или приватный ключ сюда вводить нельзя." /> ; } function EndpointDialog({ workspace, ...props }: DialogBaseProps & { workspace: ProjectWorkspace }) { const hosts = workspace.ontology.hosts; const [hostRef, setHostRef] = useState(hosts[0]?.hostRef ?? ""); const [endpointKey, setEndpointKey] = useState(""); const [purpose, setPurpose] = useState<"management" | "service" | "monitoring">("management"); const [endpointUri, setEndpointUri] = useState(""); useEffect(() => { if (!hosts.some((item) => item.hostRef === hostRef)) setHostRef(hosts[0]?.hostRef ?? ""); }, [hostRef, hosts]); return { await ensureInfrastructureEndpoint({ projectRef: workspace.project.projectRef, hostRef, endpointKey, purpose, endpointUri, lifecycleState: "active", }); }}> setEndpointUri(event.target.value)} required placeholder="ssh://203.0.113.10:22/" description="HTTPS, SSH или TCP. URI с userinfo, query или fragment будет отклонён." /> ; } function DeploymentDialog({ workspace, ...props }: DialogBaseProps & { workspace: ProjectWorkspace }) { const hosts = workspace.ontology.hosts; const [hostRef, setHostRef] = useState(hosts[0]?.hostRef ?? ""); const [deploymentKey, setDeploymentKey] = useState(""); const [displayName, setDisplayName] = useState(""); const [artifactRef, setArtifactRef] = useState(""); const [artifactDigest, setArtifactDigest] = useState(""); useEffect(() => { if (!hosts.some((item) => item.hostRef === hostRef)) setHostRef(hosts[0]?.hostRef ?? ""); }, [hostRef, hosts]); return { await ensureInfrastructureDeployment({ projectRef: workspace.project.projectRef, hostRef, deploymentKey, displayName, artifactRef, artifactDigest, lifecycleState: "active", }); }}> ({ value: item.hostRef, label: item.displayName }))} /> ({ value: item.edgeRef, label: item.displayName, description: item.channel.runtimeState })), ]} /> setDisplayName(event.target.value)} required /> setServiceRole(event.target.value.toLowerCase())} required placeholder="device.edge" /> ; } function HealthObservationDialog({ workspace, ...props }: DialogBaseProps & { workspace: ProjectWorkspace }) { const targets = [ ...workspace.ontology.hosts.map((item) => ({ value: `host|${item.hostRef}`, label: item.displayName, description: "Host" })), ...workspace.ontology.serviceInstances.map((item) => ({ value: `service-instance|${item.serviceInstanceRef}`, label: item.displayName, description: "Service instance" })), ]; const [target, setTarget] = useState(targets[0]?.value ?? ""); const [observedState, setObservedState] = useState<"reachable" | "degraded" | "unreachable">("reachable"); const [ttlMinutes, setTtlMinutes] = useState("5"); const [evidence, setEvidence] = useState("{}"); useEffect(() => { if (!targets.some((item) => item.value === target)) setTarget(targets[0]?.value ?? ""); }, [target, targets]); return { const [subjectKind, subjectRef] = target.split("|"); const observedAt = new Date(); const ttl = Number(ttlMinutes); await recordHealthObservation({ projectRef: workspace.project.projectRef, subjectKind: subjectKind as "host" | "service-instance", subjectRef, observedState, evidenceClass: "manual", sourceRef: "device-manager:manual-observation", schemaRef: "nodedc.health.manual.v1", evidence: JSON.parse(evidence) as Record, observedAt: observedAt.toISOString(), expiresAt: new Date(observedAt.valueOf() + ttl * 60_000).toISOString(), }); }}> setTtlMinutes(event.target.value)} required inputMode="numeric" /> setEvidence(event.target.value)} required />

Это ручное наблюдение с TTL. Автоматический probe должен писать тот же canonical contract от собственного source ref.

; } function EdgeDialog(props: DialogBaseProps) { const [edgeKey, setEdgeKey] = useState(""); const [displayName, setDisplayName] = useState(""); const [deploymentRef, setDeploymentRef] = useState(""); return { await ensureEdge({ edgeKey, displayName, deploymentRef: deploymentRef || null, lifecycleState: "provisioning" }); }}> setDisplayName(event.target.value)} required /> setDeploymentRef(event.target.value)} description="Opaque artifact/deployment reference, не адрес и не credential." /> ; } function RouteDialog({ workspace, ...props }: DialogBaseProps & { workspace: ProjectWorkspace }) { const [routeKey, setRouteKey] = useState(""); const [displayName, setDisplayName] = useState(""); const [edgeRef, setEdgeRef] = useState(workspace.edges[0]?.edgeRef ?? ""); const [profileRef, setProfileRef] = useState(workspace.modelProfiles[0]?.modelProfileRef ?? ""); const [listenerRef, setListenerRef] = useState(""); const profile = workspace.modelProfiles.find((item) => item.modelProfileRef === profileRef); useEffect(() => { if (!workspace.edges.some((item) => item.edgeRef === edgeRef)) { setEdgeRef(workspace.edges[0]?.edgeRef ?? ""); } if (!workspace.modelProfiles.some((item) => item.modelProfileRef === profileRef)) { setProfileRef(workspace.modelProfiles[0]?.modelProfileRef ?? ""); } }, [edgeRef, profileRef, workspace.edges, workspace.modelProfiles]); return { await ensureRoute({ projectRef: workspace.project.projectRef, routeKey, displayName, edgeRef, modelProfileRef: profileRef, listenerRef, protocol: profile?.protocol || "INTERNAL", direction: "telemetry", lifecycleState: "draft", }); }}> setDisplayName(event.target.value)} required /> ({ value: item.modelProfileRef, label: `${item.vendor} ${item.model}`, description: item.protocol }))} /> setListenerRef(event.target.value)} required />

Маршрут создаётся draft. Его activation остаётся отдельным осознанным изменением данных.

; } function BindingDialog({ workspace, ...props }: DialogBaseProps & { workspace: ProjectWorkspace }) { const sources = useMemo(() => [ ...workspace.collections.map((item) => ({ value: `collection|${item.collectionRef}`, label: item.name })), ...workspace.devices.map((item) => ({ value: `device|${item.deviceRef}`, label: item.displayName })), ], [workspace]); const [sourceValue, setSourceValue] = useState(sources[0]?.value ?? ""); const [bindingKey, setBindingKey] = useState(""); const [displayName, setDisplayName] = useState(""); const [targetKind, setTargetKind] = useState(""); const [targetRef, setTargetRef] = useState(""); const [capabilities, setCapabilities] = useState("observe"); useEffect(() => { if (!sources.some((item) => item.value === sourceValue)) { setSourceValue(sources[0]?.value ?? ""); } }, [sourceValue, sources]); return { const [kind, ref] = sourceValue.split("|"); await ensureDeviceBinding({ projectRef: workspace.project.projectRef, bindingKey, displayName, source: { kind: kind as "device" | "collection", ref }, targetKind, targetRef, capabilities: commaList(capabilities), }); }}> setPrincipalRef(event.target.value)} required /> ({ value: item.deviceRef, label: item.displayName, description: item.modelProfileRef }))} /> setConfiguration(event.target.value)} required /> setSummary(event.target.value)} />

Secret-like keys и значения будут отклонены Core. Сохранение desired не выставляет applied.

; } interface DialogBaseProps { open: boolean; onClose: () => void; onCreated: () => Promise; onError: (reason: unknown) => void; } function FormWindow({ open, onClose, onCreated, onError, id, title, submit, disabled = false, children }: DialogBaseProps & { id: string; title: string; submit: () => Promise; disabled?: boolean; children: ReactNode; }) { const [pending, setPending] = useState(false); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); setPending(true); try { await submit(); await onCreated(); } catch (reason) { onError(reason); } finally { setPending(false); } }; return ( }> {children} ); } function KeyField({ label, value, onChange }: { label: string; value: string; onChange: (value: string) => void }) { return onChange(event.target.value.toLowerCase())} required pattern="[a-z][a-z0-9-]{1,62}" />; } function ControlStack({ children }: { children: ReactNode }) { return
{children}
; } function ControlToolbar({ copy, actions }: { copy: string; actions?: ReactNode }) { return

{copy}

{actions ?
{actions}
: null}
; } function ControlSection({ title, count, children }: { title: string; count: number; children: ReactNode }) { return

{title}

{count}
{children}
; } function ResourceGrid({ children, empty }: { children: ReactNode; empty: string }) { const hasChildren = Array.isArray(children) ? children.length > 0 : Boolean(children); return hasChildren ?
{children}
:
{empty}
; } function ResourceCard({ eyebrow, title, description, status, meta, action = null }: { eyebrow: string; title: string; description: string; status: string; meta: string[]; action?: ReactNode }) { return {status}{action}}> {meta.length ?
{meta.map((item) => {item})}
:

Metadata-only projection

}
; } function ResourceList({ children, empty }: { children: ReactNode; empty: string }) { const hasChildren = Array.isArray(children) ? children.length > 0 : Boolean(children); return hasChildren ?
{children}
:
{empty}
; } function ResourceRow({ title, description, status, trailing }: { title: string; description: string; status: string; trailing: ReactNode }) { return {title}{description} {status}{typeof trailing === "string" ? {trailing} : trailing} ; } function PolicyCard({ label, value }: { label: string; value: string }) { return {label}{value}; } function commaList(value: string) { return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))].sort(); } function statusTone(status: string): "neutral" | "success" | "warning" | "danger" { if (["active", "accepted", "online", "verified", "applied", "recorded", "immutable"].includes(status)) return "success"; if (["absent", "failed", "rejected", "revoked", "retired"].includes(status)) return "danger"; if (["connecting", "draft", "provisioning", "pending", "pending_external_approval", "unknown", "unobserved", "disabled"].includes(status)) return "warning"; return "neutral"; } function formatDate(value: string | null) { if (!value) return "—"; return new Intl.DateTimeFormat("ru-RU", { dateStyle: "short", timeStyle: "short" }).format(new Date(value)); } function formatBytes(value: number) { if (value < 1024) return `${value} B`; if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KiB`; return `${(value / (1024 * 1024)).toFixed(1)} MiB`; } function shortRef(value: string | null) { return value ? `${value.slice(0, 18)}…` : "—"; } function shortDigest(value: string) { return `${value.slice(0, 15)}…${value.slice(-8)}`; }