From ebc264eb6087f44ef7291314e043209b6c179a06 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 23 Aug 2026 11:58:58 +0300 Subject: [PATCH] feat(device-core): ship host telemetry and scalable VPS inventory --- .../device-manager-presentation.test.mjs | 68 ++ .../device-manager/src/DeviceControlViews.tsx | 640 ++++++++------ apps/device-manager/src/styles.css | 807 ++++++++++++++---- .../device-control-core-release-v4.json | 45 + deployment/device-manager-release-v10.json | 56 ++ deployment/device-manager-release-v11.json | 63 ++ deployment/device-manager-release-v12.json | 67 ++ deployment/device-manager-release-v13.json | 74 ++ ...d-device-control-core-release-artifact.mjs | 60 +- ...-device-manager-control-plane-artifact.mjs | 142 ++- infra/deploy-runner/nodedc-b2-vps-deploy | 13 +- .../test_device_edge_vps_artifact.py | 47 + ..._device_manager_control_plane_artifacts.py | 340 ++++++++ 13 files changed, 1983 insertions(+), 439 deletions(-) create mode 100644 deployment/device-control-core-release-v4.json create mode 100644 deployment/device-manager-release-v10.json create mode 100644 deployment/device-manager-release-v11.json create mode 100644 deployment/device-manager-release-v12.json create mode 100644 deployment/device-manager-release-v13.json diff --git a/apps/device-manager/server/device-manager-presentation.test.mjs b/apps/device-manager/server/device-manager-presentation.test.mjs index f426d65..b83f6ca 100644 --- a/apps/device-manager/server/device-manager-presentation.test.mjs +++ b/apps/device-manager/server/device-manager-presentation.test.mjs @@ -84,6 +84,74 @@ test("Device Core overview follows the Mission Core landing-stage geometry", asy assert.ok(!client.includes(' { + const client = await readFile( + new URL("../src/DeviceControlViews.tsx", import.meta.url), + "utf8", + ); + const styles = await readFile( + new URL("../src/styles.css", import.meta.url), + "utf8", + ); + const infrastructureSource = client.slice( + client.indexOf("function HostsView"), + client.indexOf("function SessionsView"), + ); + + for (const expected of [ + 'className="infrastructure-system-workspace"', + 'className="infrastructure-system-workspace host-monitoring-workspace"', + 'className="infrastructure-overview-block"', + 'className="infrastructure-section infrastructure-hosts-block"', + 'className="infrastructure-host-list"', + 'className="infrastructure-host-card__summary"', + 'className="infrastructure-host-card__freshness"', + 'className="infrastructure-host-card__toggle"', + 'className="infrastructure-host-card__details"', + 'className="infrastructure-host-relations"', + 'aria-expanded={expanded}', + 'const [expandedHostRefs, setExpandedHostRefs] = useState>', + 'topology.endpoints.filter((item) => item.hostRef === host.hostRef)', + 'topology.deployments.filter((item) => item.hostRef === host.hostRef)', + 'topology.serviceInstances.filter((item) => item.hostRef === host.hostRef)', + 'className="host-monitoring-series-grid"', + 'className="host-monitoring-hardware"', + 'className="host-monitoring-hardware-facts"', + 'className="infrastructure-runtime-grid"', + 'percentageTelemetryWindow(cpuHistory, 5)', + 'percentageTelemetryWindow(memoryHistory, 4)', + 'className="host-monitoring-series__range"', + 'resource={networkRate.received == null ? "нет данных" : "входящий трафик"}', + 'if (!counters.length) return null;', + ]) assert.ok(infrastructureSource.includes(expected), expected); + + for (const expected of [ + ".infrastructure-system-workspace {", + ".infrastructure-overview-block,", + ".infrastructure-hosts-block,", + ".infrastructure-host-card__summary {", + "align-items: center;", + ".infrastructure-host-card__freshness[data-freshness=\"fresh\"] {", + ".infrastructure-host-card[data-expanded=\"true\"] .infrastructure-host-card__toggle svg {", + ".infrastructure-host-relations {", + "grid-template-columns: repeat(3, minmax(0, 1fr));", + "grid-template-columns: repeat(4, minmax(0, 1fr));", + "background: var(--infrastructure-panel-soft);", + ".host-monitoring-series polyline {", + "stroke: var(--nodedc-text-primary);", + ".host-monitoring-hardware-facts {", + "grid-template-columns: repeat(2, minmax(0, 1fr));", + ]) assert.ok(styles.includes(expected), expected); + + assert.ok(!infrastructureSource.includes(" { const environment = normalizeEnvironmentPresentation({ defaultTeaser: { diff --git a/apps/device-manager/src/DeviceControlViews.tsx b/apps/device-manager/src/DeviceControlViews.tsx index ce4e7bd..e8edc04 100644 --- a/apps/device-manager/src/DeviceControlViews.tsx +++ b/apps/device-manager/src/DeviceControlViews.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from "react"; +import { useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from "react"; import { Button, GlassSurface, @@ -497,10 +497,24 @@ function HostsView({ onError: (reason: unknown) => void; }) { const [selectedHostRef, setSelectedHostRef] = useState(null); + const [expandedHostRefs, setExpandedHostRefs] = useState>(() => new Set()); + const inventoryRef = useRef(null); const topology = workspace.ontology; const selectedHost = selectedHostRef ? topology.hosts.find((host) => host.hostRef === selectedHostRef) ?? null : null; + useEffect(() => { + if (selectedHostRef) return; + resetApplicationPanelScroll(inventoryRef.current); + }, [selectedHostRef]); + const toggleHost = (hostRef: string) => { + setExpandedHostRefs((current) => { + const next = new Set(current); + if (next.has(hostRef)) next.delete(hostRef); + else next.add(hostRef); + return next; + }); + }; if (selectedHost) { 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 ? ( - +
+
+
+
+ ИНФРАСТРУКТУРА / VPS И ХОСТЫ +

VPS и хосты

+

Вычислительные узлы проекта, их подключения и запущенные сервисы.

+
+ {canManageInfrastructure ? ( +
+ +
+ ) : null} +
+
+ host.telemetry.freshness === "fresh").length} со свежими данными`} /> + + + +
+
+ +
+ + + + + + : null} + /> + {topology.hosts.length ? ( +
+ {topology.hosts.map((host) => { + const hostEndpoints = topology.endpoints.filter((item) => item.hostRef === host.hostRef); + const hostDeployments = topology.deployments.filter((item) => item.hostRef === host.hostRef); + const hostServices = topology.serviceInstances.filter((item) => item.hostRef === host.hostRef); + const expanded = expandedHostRefs.has(host.hostRef); + const detailsId = `infrastructure-host-details-${host.hostRef.replace(/[^A-Za-z0-9_-]/g, "-")}`; + return ( +
+
+
+ COMPUTE HOST +

{host.displayName}

+ {host.externalRef || host.hostKey} +
+
+ + + toggleHost(host.hostRef)} + > + + +
+
+ {expanded ? ( +
+
+
Состояние
{host.lifecycleState}
+
Провайдер
{host.providerRef || "Не указан"}
+
Доступ управления
{host.managementCredentialConfigured ? "Настроен" : "Не настроен"}
+
Последнее наблюдение
{formatDate(host.telemetry.observedAt)}
+
+
+
+
ENDPOINTS{hostEndpoints.length}
+ {hostEndpoints.length ?
{hostEndpoints.map((endpoint) => ( + + ))}
:

Точки подключения не зарегистрированы.

} +
+
+
DEPLOYMENTS{hostDeployments.length}
+ {hostDeployments.length ?
{hostDeployments.map((deployment) => ( + + ))}
:

Развёртывания не зарегистрированы.

} +
+
+
SERVICES{hostServices.length}
+ {hostServices.length ?
{hostServices.map((service) => { + const edge = service.edgeRef ? workspace.edges.find((item) => item.edgeRef === service.edgeRef) : null; + const runtimeState = edge?.channel.runtimeState || service.health.state; + return ; + })}
:

Сервисы не зарегистрированы.

} +
+
+
) : 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 аудита. -

-
- +
+ ); + })} +
+ ) :
VPS и хосты для проекта пока не зарегистрированы.
} +
+ +
+ + {canManageAssets ? : null} + {canManageBindings ? : null} + } + /> + {topology.assets.length ? ( +
+ {topology.assets.map((asset) => { + const activeBindings = topology.assetBindings.filter((binding) => binding.assetRef === asset.assetRef && !binding.validTo); + return ( + +
{asset.assetTypeRef}{asset.displayName}{asset.assetKey}
+
{asset.lifecycleState}{activeBindings.length} активных привязок
+
+ ); + })} +
+ ) :
Объекты проекта пока не созданы.
} + {topology.assetBindings.length ? ( +
+ {topology.assetBindings.map((binding) => ( +
+
DEVICE ↔ ASSET{binding.deviceName} → {binding.assetName}{binding.bindingKind} · {formatDate(binding.validFrom)}
+
{binding.validTo ? "Закрыта" : "Активна"}{!binding.validTo && canManageBindings ? : null}
+
+ ))} +
+ ) : null} +
+
); } @@ -665,6 +689,12 @@ function HostTelemetryWorkspace({ onPoll: () => Promise; onError: (reason: unknown) => void; }) { + const workspaceRef = useRef(null); + + useEffect(() => { + resetApplicationPanelScroll(workspaceRef.current); + }, [host.hostRef]); + useEffect(() => { let active = true; const timer = window.setInterval(() => { @@ -683,134 +713,262 @@ function HostTelemetryWorkspace({ const memoryHistory = telemetry.history.map((sample) => sample.memoryUsedPercent); const receiveHistory = calculateNetworkRateHistory(telemetry.history, "received"); const sendHistory = calculateNetworkRateHistory(telemetry.history, "sent"); + const cpuDomain = percentageTelemetryWindow(cpuHistory, 5); + const memoryDomain = percentageTelemetryWindow(memoryHistory, 4); const runtimeServices = current?.services ?? []; + const networkInterfaces = current?.network.filter((item) => item.interface !== "lo") ?? []; return ( -
-
- - - -
- СИСТЕМА / ВЫЧИСЛИТЕЛЬНЫЕ МОДУЛИ +
+
+
+ СИСТЕМА / ВЫЧИСЛИТЕЛЬНЫЕ МОДУЛИ

{host.displayName}

-

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

+

Аппаратный и процессинговый срез выбранного VPS. Последнее обновление: {formatDate(telemetry.observedAt)}.

-
- - {telemetry.freshness === "fresh" ? "Свежие данные" : telemetry.freshness === "stale" ? "Данные устарели" : "Нет данных"} - +
+ {freshnessLabel(telemetry.freshness)} 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)} +
+ {(current?.disks ?? []).map((disk, index) => ( +
+ Диск {disk.mount ?? disk.device ?? "—"} + {formatUsedTotal(disk.usedBytes, disk.totalBytes)}
))} - {!runtimeServices.length ?
Состояние сервисов ещё не поступило.
: null} + {!current?.disks.length ?
Данные о дисках ещё не поступили.
: null}
+ + +
+ + {networkInterfaces.length ? ( +
+ {networkInterfaces.map((item, index) => ( + +
{item.interface ?? "Интерфейс"}{formatPackets(item.packetsReceived, item.packetsSent)}
+
+
Получено
{formatMetricBytes(item.bytesReceived)}
+
Отправлено
{formatMetricBytes(item.bytesSent)}
+
Ошибки RX / TX
{formatNullable(item.errorsReceived)} / {formatNullable(item.errorsSent)}
+
Потери RX / TX
{formatNullable(item.droppedReceived)} / {formatNullable(item.droppedSent)}
+
+
+ ))} +
+ ) :
Сетевые счётчики ещё не поступили.
}
-
-
ONTOLOGY / OBSERVATION{telemetry.observation?.entityId ?? "observation.observation"}
-
Источник{current ? `${current.source.agent} ${current.source.agentVersion}` : "—"}
-
Последнее наблюдение{formatDate(telemetry.observedAt)}
-
Связанные сервисы{services.length}
+
+ service.activeState === "failed") ? "danger" : runtimeServices.length ? "success" : "warning"} + /> + {runtimeServices.length ? ( +
+ {runtimeServices.map((service, index) => ( + +
+
SYSTEMD UNIT

{service.name ?? "systemd unit"}

{service.subState ?? "—"}
+ {service.activeState ?? "unknown"} +
+
+
Load
{service.loadState ?? "—"}
+
Память
{formatMetricBytes(service.memoryBytes)}
+
Перезапуски
{formatNullable(service.restarts)}
+
PID
{formatNullable(service.pid)}
+
+
+ ))} +
+ ) :
Состояние сервисов ещё не поступило.
}
); } -function TelemetryMetricCard({ label, value, points, detail }: { label: string; value: string; points: Array; detail: string }) { +function InfrastructureCount({ label, value, detail }: { label: string; value: number; detail: string }) { return ( -
- {label} - {value} - - {detail} +
+ {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)}`; +function InfrastructureSectionHeading({ eyebrow, title, description, status, statusTone: tone = "neutral", actions = null }: { + eyebrow: string; + title: string; + description: string; + status: string; + statusTone?: "neutral" | "success" | "warning" | "danger"; + actions?: ReactNode; +}) { + return ( +
+
{eyebrow}

{title}

{description}

+
{status}{actions}
+
+ ); +} + +function InfrastructureRegistryRow({ label, title, description, status }: { label: string; title: string; description: string; status: string }) { + return ( +
+
{label}{title}{description}
+ {status} +
+ ); +} + +type TelemetryLineDomain = { + minimum: number; + maximum: number; + label?: string; +}; + +function HostTelemetrySeries({ label, values, value, resource, domain }: { label: string; values: Array; value: string; resource?: string | null; domain?: TelemetryLineDomain | null }) { + const points = telemetryLinePoints(values, domain); + return ( +
+
+ {label}{resource ? {resource} : null} + {value} +
+ + + {points ? : null} + + {domain?.label ? {domain.label} : null} +
+ ); +} + +function telemetryLinePoints(values: Array, domain?: TelemetryLineDomain | null) { + const finite = values.filter((value): value is number => value !== null && Number.isFinite(value)); + if (!finite.length) return ""; + const minimum = domain?.minimum ?? 0; + const maximum = Math.max(domain?.maximum ?? Math.max(...finite, 1), minimum + Number.EPSILON); + const range = maximum - minimum; + const denominator = Math.max(1, values.length - 1); + return values.flatMap((value, index) => { + if (value === null || !Number.isFinite(value)) return []; + const x = index / denominator * 100; + const normalized = Math.max(minimum, Math.min(maximum, value)); + const y = 36 - (normalized - minimum) / range * 34; + return [`${x.toFixed(2)},${y.toFixed(2)}`]; }).join(" "); - return ; +} + +function percentageTelemetryWindow(values: Array, minimumSpan: number): TelemetryLineDomain | null { + const finite = values + .filter((value): value is number => value !== null && Number.isFinite(value)) + .map((value) => Math.max(0, Math.min(100, value))); + if (!finite.length) return null; + + const observedMinimum = Math.min(...finite); + const observedMaximum = Math.max(...finite); + const padding = Math.max(0.5, (observedMaximum - observedMinimum) * 0.15); + let minimum = observedMinimum - padding; + let maximum = observedMaximum + padding; + + if (maximum - minimum < minimumSpan) { + const center = (observedMinimum + observedMaximum) / 2; + minimum = center - minimumSpan / 2; + maximum = center + minimumSpan / 2; + } + if (minimum < 0) { + maximum = Math.min(100, maximum - minimum); + minimum = 0; + } + if (maximum > 100) { + minimum = Math.max(0, minimum - (maximum - 100)); + maximum = 100; + } + + minimum = Math.floor(minimum * 10) / 10; + maximum = Math.ceil(maximum * 10) / 10; + return { + minimum, + maximum, + label: `шкала ${formatScalePercent(minimum)}–${formatScalePercent(maximum)}`, + }; +} + +function formatScalePercent(value: number) { + return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}%`; } function TelemetryFact({ label, value }: { label: string; value: string | null | undefined }) { - return
{label}{value || "—"}
; + return
{label}
{value || "—"}
; +} + +function freshnessTone(freshness: "fresh" | "stale" | "missing"): "success" | "warning" | "danger" { + if (freshness === "fresh") return "success"; + if (freshness === "stale") return "warning"; + return "danger"; +} + +function freshnessLabel(freshness: "fresh" | "stale" | "missing") { + if (freshness === "fresh") return "Свежие данные"; + if (freshness === "stale") return "Данные устарели"; + return "Нет данных"; +} + +function russianCount(value: number, one: string, few: string, many: string) { + const absolute = Math.abs(value) % 100; + const last = absolute % 10; + const form = absolute > 10 && absolute < 20 ? many : last === 1 ? one : last > 1 && last < 5 ? few : many; + return `${new Intl.NumberFormat("ru-RU").format(value)} ${form}`; +} + +function resetApplicationPanelScroll(element: HTMLElement | null) { + const scroller = element?.closest(".nodedc-application-panel__body"); + if (scroller) scroller.scrollTop = 0; } function calculateNetworkRate(history: InfrastructureHostView["telemetry"]["history"]) { @@ -821,9 +979,10 @@ function calculateNetworkRate(history: InfrastructureHostView["telemetry"]["hist if (!Number.isFinite(seconds) || seconds <= 0) return { received: null, sent: null }; const previousTotals = networkTotals(previous.network); const latestTotals = networkTotals(latest.network); + if (!previousTotals || !latestTotals) return { received: null, sent: null }; return { - received: nonNegativeRate(latestTotals.received - previousTotals.received, seconds), - sent: nonNegativeRate(latestTotals.sent - previousTotals.sent, seconds), + received: latestTotals.received == null || previousTotals.received == null ? null : nonNegativeRate(latestTotals.received - previousTotals.received, seconds), + sent: latestTotals.sent == null || previousTotals.sent == null ? null : nonNegativeRate(latestTotals.sent - previousTotals.sent, seconds), }; } @@ -834,15 +993,20 @@ function calculateNetworkRateHistory(history: InfrastructureHostView["telemetry" if (seconds <= 0) return null; const currentTotals = networkTotals(sample.network); const previousTotals = networkTotals(previous.network); + if (!currentTotals || !previousTotals || currentTotals[direction] == null || previousTotals[direction] == null) return null; 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 }); + const counters = network.filter((item) => item.interface !== "lo" && (item.bytesReceived != null || item.bytesSent != null)); + if (!counters.length) return null; + const receivedCounters = counters.map((item) => item.bytesReceived).filter((value): value is number => value !== null && Number.isFinite(value)); + const sentCounters = counters.map((item) => item.bytesSent).filter((value): value is number => value !== null && Number.isFinite(value)); + return { + received: receivedCounters.length ? receivedCounters.reduce((total, value) => total + value, 0) : null, + sent: sentCounters.length ? sentCounters.reduce((total, value) => total + value, 0) : null, + }; } function nonNegativeRate(bytes: number, seconds: number) { diff --git a/apps/device-manager/src/styles.css b/apps/device-manager/src/styles.css index 6095119..1444c94 100644 --- a/apps/device-manager/src/styles.css +++ b/apps/device-manager/src/styles.css @@ -127,231 +127,655 @@ padding-right: 3px; } -.host-telemetry-workspace { +.infrastructure-system-workspace { + --infrastructure-panel-soft: var(--nodedc-canvas-soft); + --infrastructure-accent-soft: color-mix(in srgb, var(--nodedc-text-primary) 4.5%, transparent); + --infrastructure-hairline: color-mix(in srgb, var(--nodedc-text-primary) 8%, transparent); display: grid; - gap: 18px; min-width: 0; + gap: 1rem; + padding-bottom: 1rem; } -.host-telemetry-header { - display: grid; - grid-template-columns: auto minmax(0, 1fr) auto; - align-items: start; - gap: 14px; +.infrastructure-overview-block, +.infrastructure-hosts-block, +.infrastructure-assets-block { + min-width: 0; + border-radius: var(--nodedc-radius-card); + background: var(--infrastructure-panel-soft); + padding: 1rem; } -.host-telemetry-header__copy { - display: grid; - gap: 3px; +.infrastructure-system-workspace .nodedc-status { + min-height: auto; + justify-content: flex-start; + gap: 0.42rem; + border-radius: 0; + background: transparent; + color: var(--nodedc-text-primary); + padding: 0; + font-size: var(--nodedc-font-size-sm); + font-weight: var(--nodedc-font-weight-medium); } -.host-telemetry-header__copy small, -.host-telemetry-section__heading small, -.host-telemetry-evidence small { - color: var(--nodedc-text-tertiary); +.infrastructure-system-workspace .nodedc-status::before { + width: 0.42rem; + height: 0.42rem; + flex: 0 0 0.42rem; + border-radius: 50%; + background: var(--nodedc-text-muted); + content: ""; +} + +.infrastructure-system-workspace .nodedc-status[data-tone="success"], +.infrastructure-system-workspace .nodedc-status[data-tone="warning"], +.infrastructure-system-workspace .nodedc-status[data-tone="danger"], +.infrastructure-system-workspace .nodedc-status[data-tone="accent"] { + background: transparent; + color: var(--nodedc-text-primary); +} + +.infrastructure-system-workspace .nodedc-status[data-tone="success"]::before { + background: rgb(var(--nodedc-success-rgb)); +} + +.infrastructure-system-workspace .nodedc-status[data-tone="warning"]::before { + background: rgb(var(--nodedc-warning-rgb)); +} + +.infrastructure-system-workspace .nodedc-status[data-tone="danger"]::before { + background: rgb(var(--nodedc-danger-rgb)); +} + +.infrastructure-system-workspace .nodedc-status[data-tone="accent"]::before { + background: var(--nodedc-text-primary); +} + +.infrastructure-eyebrow { + display: block; + color: var(--nodedc-text-muted); font-size: 0.62rem; - font-weight: 780; + font-weight: 820; letter-spacing: 0.12em; + line-height: 1.2; text-transform: uppercase; } -.host-telemetry-header__copy h2, -.host-telemetry-header__copy p, -.host-telemetry-section__heading h3, -.host-telemetry-section__heading p { - margin: 0; +.infrastructure-workspace-lead, +.infrastructure-section-heading { + display: flex; + min-width: 0; + align-items: flex-start; + justify-content: space-between; + gap: 1.2rem; } -.host-telemetry-header__copy h2 { - font-size: 1.4rem; +.infrastructure-workspace-lead { + padding: 0; +} + +.infrastructure-workspace-lead h2, +.infrastructure-section-heading h3 { + margin: 0.4rem 0 0; + color: var(--nodedc-text-primary); letter-spacing: -0.035em; } -.host-telemetry-header__copy p, -.host-telemetry-section__heading p { - color: var(--nodedc-text-tertiary); - font-size: 0.76rem; +.infrastructure-workspace-lead h2 { + font-size: 1.42rem; +} + +.infrastructure-section-heading h3 { + font-size: 1.02rem; +} + +.infrastructure-workspace-lead p, +.infrastructure-section-heading p { + max-width: 48rem; + margin: 0.42rem 0 0; + color: var(--nodedc-text-muted); + font-size: 0.68rem; + line-height: 1.48; +} + +.infrastructure-workspace-actions, +.infrastructure-section-actions { + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 0.55rem; +} + +.infrastructure-overview-grid, +.host-monitoring-series-grid { + display: grid; + min-width: 0; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.65rem; +} + +.infrastructure-overview-block .infrastructure-overview-grid { + margin-top: 1rem; +} + +.infrastructure-count-card, +.host-monitoring-series { + min-width: 0; + border-radius: var(--nodedc-radius-card); + background: var(--infrastructure-panel-soft); +} + +.infrastructure-count-card { + display: grid; + align-content: center; + gap: 0.3rem; + min-height: 5.4rem; + padding: 0.82rem; + background: var(--infrastructure-accent-soft); +} + +.infrastructure-count-card span, +.infrastructure-count-card small, +.host-monitoring-series span { + color: var(--nodedc-text-muted); + font-size: 0.62rem; +} + +.infrastructure-count-card strong, +.host-monitoring-series strong { + color: var(--nodedc-text-primary); + font-size: 1.14rem; + font-weight: 720; + letter-spacing: -0.035em; + white-space: nowrap; +} + +.infrastructure-count-card small { + overflow: hidden; + font-size: 0.55rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.infrastructure-section { + display: grid; + min-width: 0; + gap: 0.75rem; +} + +.infrastructure-section--separated { + padding-top: 0.25rem; + border-top: 1px solid var(--infrastructure-hairline); +} + +.infrastructure-runtime-grid, +.infrastructure-asset-grid, +.host-monitoring-network-grid { + display: grid; + min-width: 0; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.7rem; +} + +.infrastructure-host-list { + display: grid; + min-width: 0; + gap: 0.5rem; +} + +.infrastructure-host-card, +.infrastructure-runtime-card, +.host-monitoring-runtime-card, +.host-monitoring-network-card { + display: grid; + min-width: 0; + gap: 0.9rem; +} + +.infrastructure-host-card { + overflow: hidden; + border-radius: var(--nodedc-radius-control); + background: var(--infrastructure-accent-soft); +} + +.infrastructure-host-card__summary, +.infrastructure-runtime-card > header, +.host-monitoring-runtime-card > header { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 0.8rem; +} + +.infrastructure-host-card__summary { + min-height: 4.35rem; + padding: 0.62rem 0.72rem; +} + +.infrastructure-host-card__identity { + min-width: 0; +} + +.infrastructure-host-card__actions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 0.6rem; +} + +.infrastructure-host-card__freshness { + width: 0.48rem; + height: 0.48rem; + flex: 0 0 0.48rem; + border-radius: 50%; + background: var(--nodedc-text-muted); +} + +.infrastructure-host-card__freshness[data-freshness="fresh"] { + background: rgb(var(--nodedc-success-rgb)); +} + +.infrastructure-host-card__freshness[data-freshness="stale"] { + background: rgb(var(--nodedc-warning-rgb)); +} + +.infrastructure-host-card__toggle svg { + transition: transform 160ms ease; +} + +.infrastructure-host-card[data-expanded="true"] .infrastructure-host-card__toggle svg { + transform: rotate(180deg); +} + +.infrastructure-host-card h4, +.infrastructure-runtime-card h3, +.host-monitoring-runtime-card h3 { + margin: 0.38rem 0 0.18rem; + color: var(--nodedc-text-primary); + font-size: 0.9rem; + letter-spacing: -0.025em; +} + +.infrastructure-host-card code, +.infrastructure-runtime-card code, +.host-monitoring-runtime-card code { + display: block; + max-width: 23rem; + overflow: hidden; + color: var(--nodedc-text-muted); + font-size: 0.56rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.infrastructure-host-card__details { + display: grid; + min-width: 0; + gap: 0.8rem; + padding: 0 0.72rem 0.72rem; +} + +.infrastructure-host-card__facts { + display: grid; + min-width: 0; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin: 0; + padding: 0.72rem; + border-radius: var(--nodedc-radius-control); + background: var(--infrastructure-panel-soft); +} + +.infrastructure-host-card__facts > div { + display: grid; + min-width: 0; + gap: 0.22rem; + padding-right: 0.65rem; +} + +.infrastructure-host-card__facts dt { + color: var(--nodedc-text-muted); + font-size: 0.58rem; +} + +.infrastructure-host-card__facts dd { + overflow: hidden; + margin: 0; + color: var(--nodedc-text-primary); + font-size: 0.67rem; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.infrastructure-host-relations { + display: grid; + min-width: 0; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.65rem; +} + +.infrastructure-host-relations > section { + display: grid; + min-width: 0; + align-content: start; + gap: 0.55rem; + padding: 0.72rem; + border-radius: var(--nodedc-radius-control); + background: var(--infrastructure-panel-soft); +} + +.infrastructure-host-relations > section > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.65rem; +} + +.infrastructure-host-relations > section > header strong { + color: var(--nodedc-text-primary); + font-size: 0.68rem; +} + +.infrastructure-host-relations > section > p { + margin: 0; + color: var(--nodedc-text-muted); + font-size: 0.58rem; + line-height: 1.45; +} + +.infrastructure-host-relations .infrastructure-registry-row { + padding: 0.58rem 0.62rem; +} + +.infrastructure-host-relations .infrastructure-registry-row .nodedc-status { + font-size: 0; +} + +.infrastructure-host-relations .infrastructure-registry-row .nodedc-status::before { + margin: 0; +} + +.infrastructure-host-card dl, +.infrastructure-runtime-card dl, +.host-monitoring-runtime-card dl, +.host-monitoring-network-card dl { + display: grid; + min-width: 0; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin: 0; + gap: 0; +} + +.infrastructure-host-card dl > div, +.infrastructure-runtime-card dl > div, +.host-monitoring-runtime-card dl > div, +.host-monitoring-network-card dl > div { + display: grid; + min-width: 0; + gap: 0.22rem; + padding-right: 0.65rem; +} + +.infrastructure-host-card dt, +.infrastructure-runtime-card dt, +.host-monitoring-runtime-card dt, +.host-monitoring-network-card dt, +.host-monitoring-hardware-facts dt { + color: var(--nodedc-text-muted); + font-size: 0.58rem; +} + +.infrastructure-host-card dd, +.infrastructure-runtime-card dd, +.host-monitoring-runtime-card dd, +.host-monitoring-network-card dd, +.host-monitoring-hardware-facts dd { + min-width: 0; + overflow: hidden; + margin: 0; + color: var(--nodedc-text-primary); + font-size: 0.67rem; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.infrastructure-host-card footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.8rem; +} + +.infrastructure-host-card footer > span { + overflow: hidden; + color: var(--nodedc-text-muted); + font-size: 0.58rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.infrastructure-registry-list { + display: grid; + min-width: 0; + gap: 0.48rem; +} + +.infrastructure-registry-row, +.infrastructure-asset-card { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 0.9rem; + padding: 0.72rem 0.8rem; + border-radius: var(--nodedc-radius-control); + background: var(--infrastructure-panel-soft); +} + +.infrastructure-registry-row > div, +.infrastructure-asset-card > div { + display: grid; + min-width: 0; + gap: 0.2rem; +} + +.infrastructure-registry-row > div:last-child, +.infrastructure-asset-card > div:last-child { + flex: 0 0 auto; + justify-items: end; +} + +.infrastructure-registry-row strong, +.infrastructure-asset-card strong { + overflow: hidden; + color: var(--nodedc-text-primary); + font-size: 0.68rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.infrastructure-registry-row small, +.infrastructure-asset-card small { + overflow: hidden; + color: var(--nodedc-text-muted); + font-size: 0.55rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.infrastructure-empty { + padding: 0.8rem; + border-radius: var(--nodedc-radius-control); + background: var(--infrastructure-panel-soft); + color: var(--nodedc-text-muted); + font-size: 0.65rem; line-height: 1.5; } -.host-telemetry-header__actions { - display: flex; - align-items: center; - gap: 8px; -} - -.host-telemetry-metric-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.host-telemetry-metric, -.host-telemetry-section { - border: 1px solid var(--nodedc-glass-outline); - border-radius: var(--nodedc-radius-lg); - background: color-mix(in srgb, var(--nodedc-surface) 82%, transparent); -} - -.host-telemetry-metric { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 5px 12px; - min-height: 112px; - padding: 14px; +.host-monitoring-series { overflow: hidden; + padding: 0.82rem 0.82rem 0.32rem; } -.host-telemetry-metric > span, -.host-telemetry-metric > small, -.host-telemetry-facts small, -.host-telemetry-list__row small, -.host-telemetry-service small { - color: var(--nodedc-text-tertiary); - font-size: 0.66rem; -} - -.host-telemetry-metric > strong { - font-size: 1.35rem; - font-weight: 620; -} - -.host-telemetry-metric > small { - grid-column: 1 / -1; -} - -.host-telemetry-sparkline { - grid-column: 1 / -1; - width: 100%; - height: 32px; - overflow: visible; -} - -.host-telemetry-sparkline polyline { - fill: none; - stroke: rgb(var(--nodedc-accent-rgb)); - stroke-width: 1.5; - vector-effect: non-scaling-stroke; -} - -.host-telemetry-sparkline--empty { - border-bottom: 1px solid var(--nodedc-glass-outline); -} - -.host-telemetry-section { - display: grid; - gap: 14px; - padding: 18px; -} - -.host-telemetry-section__heading { +.host-monitoring-series > div { display: flex; align-items: flex-start; justify-content: space-between; - gap: 16px; + gap: 0.75rem; } -.host-telemetry-section__heading > div { +.host-monitoring-series__label { display: grid; - gap: 4px; -} - -.host-telemetry-section__heading h3 { - font-size: 1rem; -} - -.host-telemetry-facts { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 1px; - overflow: hidden; - border-radius: var(--nodedc-radius-md); - background: var(--nodedc-glass-outline); -} - -.host-telemetry-facts > div { - display: grid; - gap: 5px; min-width: 0; - padding: 12px; - background: var(--nodedc-surface-soft); + gap: 0.16rem; } -.host-telemetry-facts strong { +.host-monitoring-series__label small { overflow: hidden; - font-size: 0.78rem; - font-weight: 590; + max-width: 11rem; + color: var(--nodedc-text-secondary); + font-size: 0.54rem; + line-height: 1.15; text-overflow: ellipsis; white-space: nowrap; } -.host-telemetry-split { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; +.host-monitoring-series svg { + display: block; + width: 100%; + height: 2.45rem; + margin-top: 0.45rem; } -.host-telemetry-list, -.host-telemetry-service-grid { - display: grid; - gap: 7px; +.host-monitoring-series path { + fill: none; + stroke: var(--infrastructure-hairline); + stroke-width: 0.8; + vector-effect: non-scaling-stroke; } -.host-telemetry-list__row, -.host-telemetry-service { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - min-width: 0; - padding: 10px 12px; - border-radius: var(--nodedc-radius-md); - background: var(--nodedc-surface-soft); +.host-monitoring-series polyline { + fill: none; + stroke: var(--nodedc-text-primary); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.15; + vector-effect: non-scaling-stroke; } -.host-telemetry-list__row > span, -.host-telemetry-service > span { - display: grid; - gap: 3px; - min-width: 0; -} - -.host-telemetry-list__row > span:last-child, -.host-telemetry-service > span:last-child { - justify-items: end; +.host-monitoring-series__range { + display: block; + margin-top: -0.1rem; + color: var(--nodedc-text-secondary); + font-size: 0.5rem; + line-height: 1.2; text-align: right; } -.host-telemetry-list__row strong, -.host-telemetry-service strong { - overflow: hidden; - font-size: 0.76rem; - font-weight: 590; - text-overflow: ellipsis; - white-space: nowrap; -} - -.host-telemetry-service-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); -} - -.host-telemetry-evidence { - grid-template-columns: repeat(4, minmax(0, 1fr)); -} - -.host-telemetry-evidence > div { +.host-monitoring-hardware { display: grid; - gap: 5px; min-width: 0; + gap: 1rem; } -.host-telemetry-evidence strong { +.host-monitoring-hardware-facts { + display: grid; + min-width: 0; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.7rem; +} + +.host-monitoring-hardware-facts dl { + display: grid; + min-width: 0; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 0; + gap: 0; + border-radius: var(--nodedc-radius-control); + background: var(--infrastructure-panel-soft); +} + +.host-monitoring-hardware-facts dl > div { + display: grid; + min-width: 0; + gap: 0.22rem; + padding: 0.75rem; +} + +.host-monitoring-disk-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: 0.55rem; +} + +.host-monitoring-disk-list > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.8rem; + padding: 0.62rem 0.72rem; + border-radius: var(--nodedc-radius-control); + background: var(--infrastructure-accent-soft); +} + +.host-monitoring-disk-list span { + color: var(--nodedc-text-muted); + font-size: 0.61rem; +} + +.host-monitoring-disk-list strong { + color: var(--nodedc-text-primary); + font-size: 0.64rem; +} + +.host-monitoring-network-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.65rem; +} + +.host-monitoring-network-card header strong, +.host-monitoring-network-card header small { + display: block; overflow: hidden; - font-size: 0.75rem; text-overflow: ellipsis; white-space: nowrap; } +.host-monitoring-network-card header strong { + color: var(--nodedc-text-primary); + font-size: 0.71rem; +} + +.host-monitoring-network-card header small { + margin-top: 0.25rem; + color: var(--nodedc-text-muted); + font-size: 0.55rem; +} + +.host-monitoring-network-card dl { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.65rem; +} + +@media (max-width: 1100px) { + .infrastructure-overview-grid, + .host-monitoring-series-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .host-monitoring-network-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .infrastructure-host-relations { + grid-template-columns: 1fr; + } +} + @media (max-width: 760px) { .device-control-resource-grid, .device-control-policy-grid { @@ -394,22 +818,47 @@ justify-content: flex-start; } - .host-telemetry-header { - grid-template-columns: auto minmax(0, 1fr); + .infrastructure-workspace-lead, + .infrastructure-section-heading { + display: grid; } - .host-telemetry-header__actions { - grid-column: 2; - justify-content: flex-start; + .infrastructure-workspace-actions, + .infrastructure-section-actions { + justify-content: space-between; } - .host-telemetry-metric-grid, - .host-telemetry-facts, - .host-telemetry-service-grid, - .host-telemetry-evidence, - .host-telemetry-split { + .infrastructure-overview-grid, + .host-monitoring-series-grid, + .infrastructure-host-list, + .infrastructure-runtime-grid, + .infrastructure-asset-grid, + .host-monitoring-hardware-facts, + .host-monitoring-network-grid { grid-template-columns: 1fr; } + + .infrastructure-host-card__facts, + .infrastructure-runtime-card dl, + .host-monitoring-runtime-card dl { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.65rem; + } + + .infrastructure-host-card__summary, + .infrastructure-registry-row, + .infrastructure-asset-card { + align-items: flex-start; + } + + .infrastructure-host-card__summary { + display: grid; + } + + .infrastructure-host-card__actions { + width: 100%; + justify-content: flex-end; + } } html, diff --git a/deployment/device-control-core-release-v4.json b/deployment/device-control-core-release-v4.json new file mode 100644 index 0000000..43453e3 --- /dev/null +++ b/deployment/device-control-core-release-v4.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": "nodedc.device-plane.device-control-core-release.v4", + "releaseId": "__PATCH_ID__", + "action": "upgrade", + "predecessor": { + "kind": "migration-replay-checkpoint-recovery", + "patchId": "device-control-core-migration-replay-checkpoint-recovery-20260822-046", + "artifactSha256": "46000c76977fb583fc7c9cf74ecf624efd8b404f7b8d0322e0270e7b8ac6e450" + }, + "service": "device-control-core", + "composeActivation": "preserve-active-v4-topology", + "identity": "reuse-existing-runner-managed-host-local-private-key-public-certificate-export", + "identityRecovery": "forbidden-valid-existing-identity-required", + "tlsPurpose": "clientAuth", + "direction": "core-initiated", + "endpointPolicy": "public-ipv4-standard-https-tcp-443-only", + "coreNetworks": [ + "device-plane-private", + "device-plane-egress" + ], + "publicIngress": "none-on-synology", + "edgeRegistrations": "preserved", + "commandTransport": "typed-service-ping-v1", + "commandCatalog": "allowlisted-adapter-typed-commands-only", + "credentialBoundary": "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned", + "telemetryTransport": "edge-channel-host-telemetry-observed-v1", + "telemetryContract": "nodedc.infrastructure.host-telemetry.v1", + "telemetryStorage": "device-control-core-postgres-seven-day-retention", + "ontologyProjection": "observation-observed-property-provenance-freshness-v1", + "telemetryFreshness": "fifteen-seconds-missing-stale-not-unhealthy", + "recoveryPredecessor": "terminal-applied-046-exact-source-runtime-database", + "databasePreflight": "final-migration-016-validated-and-host-telemetry-table-absent", + "databaseRowMutation": "none-before-core-startup-migrations", + "databaseSchemaOutcome": "migration-017-host-telemetry-table-present", + "runtimePredecessor": "healthy-recovery-046-core-generation", + "gelios": "untouched-legacy-only", + "preservedServices": [ + "device-manager", + "device-gateway", + "device-postgres", + "device-backhaul-target" + ], + "healthGate": "bounded-container-grace+core-edge-contract+exact-private-egress-network-boundary", + "rollback": "restore-preapply-source-and-core-runtime" +} diff --git a/deployment/device-manager-release-v10.json b/deployment/device-manager-release-v10.json new file mode 100644 index 0000000..cb9d93b --- /dev/null +++ b/deployment/device-manager-release-v10.json @@ -0,0 +1,56 @@ +{ + "schemaVersion": "nodedc.device-plane.device-manager-release.v10", + "releaseId": "__PATCH_ID__", + "action": "upgrade", + "predecessor": { + "kind": "release", + "patchId": "device-manager-release-v8-20260822-039", + "artifactSha256": "30a83d4c6b5c029c96c4af19fa558e0ae75e4b60bc897881457304bd17e0e465" + }, + "controlCorePredecessor": { + "patchId": "device-control-core-release-v4-20260823-047", + "artifactSha256": "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + }, + "edgeChannelPredecessor": { + "patchId": "device-edge-core-channel-upgrade-v4-20260812-023", + "artifactSha256": "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" + }, + "service": "device-manager", + "publicIngress": "reverse-proxy-only", + "deviceCoreManagementApi": "file-token-authenticated", + "launcherTrust": "file-token-scoped-to-device-core-handoff", + "edgeChannel": "preserve-active-v4-core-initiated-pinned-mtls", + "edgeChannelIdentity": "reuse-runner-managed-host-local-private-key-public-certificate-export", + "edgeChannelEgress": "preserve-dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-443-only", + "healthGate": "bounded-container-grace+core-contract+persistent-data", + "commandTransport": "typed-service-ping-v1", + "commandCatalog": "allowlisted-adapter-typed-commands-only", + "credentialBoundary": "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned", + "presentationPersistence": "runner-managed-host-data-bind", + "presentationDataHostPath": "/volume1/docker/nodedc-device-plane/data/device-manager", + "presentationDataContainerPath": "/var/lib/nodedc-device-manager", + "presentationDataOwnership": "uid-1000-gid-1000-mode-0750", + "presentationDataLifecycle": "preserve-across-manager-recreate-and-source-rollback", + "presentationPath": "/var/lib/nodedc-device-manager/device-manager-presentation.json", + "mediaRoot": "/var/lib/nodedc-device-manager/media", + "defaultAccentHex": "#f5f5f5", + "overviewLayout": "mission-core-landing-stage-v1", + "faviconSet": "nodedc-adaptive-v1", + "commandFormLayout": "aligned-control-row-v1", + "secondaryEmptyTypography": "help-text-sm-v1", + "infrastructureHostProjection": "ontology-backed-host-runtime-v1", + "ontologyFoundation": "ontology-core-device-foundation-20260822-001", + "ontologyCatalogHash": "229c61c02a790906", + "assetBinding": "temporal-device-asset-binding-v1", + "infrastructureRuntime": "host-endpoint-deployment-service-instance-v1", + "healthEvidence": "ttl-observation-missing-not-unhealthy-v1", + "telemetryWorkspace": "mission-core-compute-module-parity-v1", + "telemetryNavigation": "full-workspace-back-navigation-v1", + "telemetryPollInterval": "three-seconds", + "telemetryFreshness": "fifteen-seconds-missing-stale-not-unhealthy", + "telemetryOntologyProjection": "observation-observed-property-provenance-freshness-v1", + "telemetryAgent": "telegraf-host-observer-v1", + "interactiveShell": "disabled-pending-managed-session-boundary", + "gelios": "untouched-legacy-only", + "rollback": "restore-preapply-snapshot-preserve-manager-data" +} diff --git a/deployment/device-manager-release-v11.json b/deployment/device-manager-release-v11.json new file mode 100644 index 0000000..35d2b13 --- /dev/null +++ b/deployment/device-manager-release-v11.json @@ -0,0 +1,63 @@ +{ + "schemaVersion": "nodedc.device-plane.device-manager-release.v11", + "releaseId": "__PATCH_ID__", + "action": "upgrade", + "predecessor": { + "kind": "release", + "patchId": "device-manager-release-v10-20260823-048", + "artifactSha256": "e6b983a314db4f8c27d89062dfedf5ed0523cc30421170799d181a19e2d85d4c" + }, + "controlCorePredecessor": { + "patchId": "device-control-core-release-v4-20260823-047", + "artifactSha256": "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + }, + "edgeChannelPredecessor": { + "patchId": "device-edge-core-channel-upgrade-v4-20260812-023", + "artifactSha256": "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" + }, + "service": "device-manager", + "publicIngress": "reverse-proxy-only", + "deviceCoreManagementApi": "file-token-authenticated", + "launcherTrust": "file-token-scoped-to-device-core-handoff", + "edgeChannel": "preserve-active-v4-core-initiated-pinned-mtls", + "edgeChannelIdentity": "reuse-runner-managed-host-local-private-key-public-certificate-export", + "edgeChannelEgress": "preserve-dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-443-only", + "healthGate": "bounded-container-grace+core-contract+persistent-data", + "commandTransport": "typed-service-ping-v1", + "commandCatalog": "allowlisted-adapter-typed-commands-only", + "credentialBoundary": "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned", + "presentationPersistence": "runner-managed-host-data-bind", + "presentationDataHostPath": "/volume1/docker/nodedc-device-plane/data/device-manager", + "presentationDataContainerPath": "/var/lib/nodedc-device-manager", + "presentationDataOwnership": "uid-1000-gid-1000-mode-0750", + "presentationDataLifecycle": "preserve-across-manager-recreate-and-source-rollback", + "presentationPath": "/var/lib/nodedc-device-manager/device-manager-presentation.json", + "mediaRoot": "/var/lib/nodedc-device-manager/media", + "defaultAccentHex": "#f5f5f5", + "overviewLayout": "mission-core-landing-stage-v1", + "faviconSet": "nodedc-adaptive-v1", + "commandFormLayout": "aligned-control-row-v1", + "secondaryEmptyTypography": "help-text-sm-v1", + "infrastructureHostProjection": "ontology-backed-host-runtime-v1", + "ontologyFoundation": "ontology-core-device-foundation-20260822-001", + "ontologyCatalogHash": "229c61c02a790906", + "assetBinding": "temporal-device-asset-binding-v1", + "infrastructureRuntime": "host-endpoint-deployment-service-instance-v1", + "healthEvidence": "ttl-observation-missing-not-unhealthy-v1", + "designSystem": "nodedc-canonical-components-and-tokens-v1", + "missionCoreReference": "compute-modules-workspace-71c8b04", + "infrastructureWorkspaceLayout": "mission-core-system-workspace-v1", + "hostInventoryComposition": "mission-core-compute-host-list-v1", + "telemetryWorkspace": "mission-core-compute-module-visual-parity-v2", + "telemetrySurface": "borderless-soft-surface-v1", + "telemetryStatus": "mission-core-dot-status-v1", + "telemetryNavigation": "full-workspace-back-navigation-v1", + "telemetryScroll": "reset-on-workspace-transition-v1", + "telemetryPollInterval": "three-seconds", + "telemetryFreshness": "fifteen-seconds-missing-stale-not-unhealthy", + "telemetryOntologyProjection": "observation-observed-property-provenance-freshness-v1", + "telemetryAgent": "telegraf-host-observer-v1", + "interactiveShell": "disabled-pending-managed-session-boundary", + "gelios": "untouched-legacy-only", + "rollback": "restore-preapply-snapshot-preserve-manager-data" +} diff --git a/deployment/device-manager-release-v12.json b/deployment/device-manager-release-v12.json new file mode 100644 index 0000000..fbb731f --- /dev/null +++ b/deployment/device-manager-release-v12.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": "nodedc.device-plane.device-manager-release.v12", + "releaseId": "__PATCH_ID__", + "action": "upgrade", + "predecessor": { + "kind": "release", + "patchId": "device-manager-release-v11-20260823-049", + "artifactSha256": "c1e2056b50bfbb0d03d077461d0c27cc56cc52967c3f5620be14871c8a6d5cf0" + }, + "controlCorePredecessor": { + "patchId": "device-control-core-release-v4-20260823-047", + "artifactSha256": "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + }, + "edgeChannelPredecessor": { + "patchId": "device-edge-core-channel-upgrade-v4-20260812-023", + "artifactSha256": "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" + }, + "service": "device-manager", + "publicIngress": "reverse-proxy-only", + "deviceCoreManagementApi": "file-token-authenticated", + "launcherTrust": "file-token-scoped-to-device-core-handoff", + "edgeChannel": "preserve-active-v4-core-initiated-pinned-mtls", + "edgeChannelIdentity": "reuse-runner-managed-host-local-private-key-public-certificate-export", + "edgeChannelEgress": "preserve-dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-443-only", + "healthGate": "bounded-container-grace+core-contract+persistent-data", + "commandTransport": "typed-service-ping-v1", + "commandCatalog": "allowlisted-adapter-typed-commands-only", + "credentialBoundary": "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned", + "presentationPersistence": "runner-managed-host-data-bind", + "presentationDataHostPath": "/volume1/docker/nodedc-device-plane/data/device-manager", + "presentationDataContainerPath": "/var/lib/nodedc-device-manager", + "presentationDataOwnership": "uid-1000-gid-1000-mode-0750", + "presentationDataLifecycle": "preserve-across-manager-recreate-and-source-rollback", + "presentationPath": "/var/lib/nodedc-device-manager/device-manager-presentation.json", + "mediaRoot": "/var/lib/nodedc-device-manager/media", + "defaultAccentHex": "#f5f5f5", + "overviewLayout": "mission-core-landing-stage-v1", + "faviconSet": "nodedc-adaptive-v1", + "commandFormLayout": "aligned-control-row-v1", + "secondaryEmptyTypography": "help-text-sm-v1", + "infrastructureHostProjection": "ontology-backed-host-runtime-v1", + "ontologyFoundation": "ontology-core-device-foundation-20260822-001", + "ontologyCatalogHash": "229c61c02a790906", + "assetBinding": "temporal-device-asset-binding-v1", + "infrastructureRuntime": "host-endpoint-deployment-service-instance-v1", + "healthEvidence": "ttl-observation-missing-not-unhealthy-v1", + "designSystem": "nodedc-canonical-components-and-tokens-v1", + "missionCoreReference": "compute-modules-workspace-71c8b04", + "infrastructureWorkspaceLayout": "mission-core-system-workspace-v1", + "hostInventoryComposition": "mission-core-compute-host-list-v1", + "telemetryWorkspace": "mission-core-compute-module-adaptive-window-v3", + "telemetrySurface": "borderless-soft-surface-v1", + "telemetryStatus": "mission-core-dot-status-v1", + "telemetryNavigation": "full-workspace-back-navigation-v1", + "telemetryScroll": "reset-on-workspace-transition-v1", + "telemetryPollInterval": "three-seconds", + "telemetryFreshness": "fifteen-seconds-missing-stale-not-unhealthy", + "telemetryOntologyProjection": "observation-observed-property-provenance-freshness-v1", + "telemetryAgent": "telegraf-host-observer-v1", + "telemetryGraphScale": "adaptive-observed-window-explicit-domain-v1", + "telemetryCpuMinimumSpan": "five-percentage-points", + "telemetryMemoryMinimumSpan": "four-percentage-points", + "telemetryNetworkMissingSemantics": "missing-counters-never-zero-v1", + "interactiveShell": "disabled-pending-managed-session-boundary", + "gelios": "untouched-legacy-only", + "rollback": "restore-preapply-snapshot-preserve-manager-data" +} diff --git a/deployment/device-manager-release-v13.json b/deployment/device-manager-release-v13.json new file mode 100644 index 0000000..d3b7579 --- /dev/null +++ b/deployment/device-manager-release-v13.json @@ -0,0 +1,74 @@ +{ + "schemaVersion": "nodedc.device-plane.device-manager-release.v13", + "releaseId": "__PATCH_ID__", + "action": "upgrade", + "predecessor": { + "kind": "release", + "patchId": "device-manager-release-v12-20260823-050", + "artifactSha256": "1a49839140e5f2e49763d78f24ee47d946e244bcfde15a9c38266e8bd14c0d49" + }, + "controlCorePredecessor": { + "patchId": "device-control-core-release-v4-20260823-047", + "artifactSha256": "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + }, + "edgeChannelPredecessor": { + "patchId": "device-edge-core-channel-upgrade-v4-20260812-023", + "artifactSha256": "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" + }, + "service": "device-manager", + "publicIngress": "reverse-proxy-only", + "deviceCoreManagementApi": "file-token-authenticated", + "launcherTrust": "file-token-scoped-to-device-core-handoff", + "edgeChannel": "preserve-active-v4-core-initiated-pinned-mtls", + "edgeChannelIdentity": "reuse-runner-managed-host-local-private-key-public-certificate-export", + "edgeChannelEgress": "preserve-dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-443-only", + "healthGate": "bounded-container-grace+core-contract+persistent-data", + "commandTransport": "typed-service-ping-v1", + "commandCatalog": "allowlisted-adapter-typed-commands-only", + "credentialBoundary": "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned", + "presentationPersistence": "runner-managed-host-data-bind", + "presentationDataHostPath": "/volume1/docker/nodedc-device-plane/data/device-manager", + "presentationDataContainerPath": "/var/lib/nodedc-device-manager", + "presentationDataOwnership": "uid-1000-gid-1000-mode-0750", + "presentationDataLifecycle": "preserve-across-manager-recreate-and-source-rollback", + "presentationPath": "/var/lib/nodedc-device-manager/device-manager-presentation.json", + "mediaRoot": "/var/lib/nodedc-device-manager/media", + "defaultAccentHex": "#f5f5f5", + "overviewLayout": "mission-core-landing-stage-v1", + "faviconSet": "nodedc-adaptive-v1", + "commandFormLayout": "aligned-control-row-v1", + "secondaryEmptyTypography": "help-text-sm-v1", + "infrastructureHostProjection": "ontology-backed-host-runtime-v1", + "ontologyFoundation": "ontology-core-device-foundation-20260822-001", + "ontologyCatalogHash": "229c61c02a790906", + "assetBinding": "temporal-device-asset-binding-v1", + "infrastructureRuntime": "host-endpoint-deployment-service-instance-v1", + "healthEvidence": "ttl-observation-missing-not-unhealthy-v1", + "designSystem": "nodedc-canonical-components-and-tokens-v1", + "missionCoreReference": "compute-modules-workspace-71c8b04", + "infrastructureWorkspaceLayout": "mission-core-system-workspace-v2", + "hostInventoryComposition": "mission-core-compute-host-accordion-v2", + "hostInventoryOverviewSurface": "separate-summary-soft-surface-v1", + "hostInventoryCollectionSurface": "separate-host-collection-soft-surface-v1", + "hostInventoryRow": "compact-centered-accordion-v1", + "hostInventoryFreshness": "dot-only-v1", + "hostInventoryRelations": "host-scoped-endpoint-deployment-service-v1", + "hostInventoryDefaultExpansion": "collapsed", + "hostInventoryScaleTarget": "five-hundred-collapsed-rows-v1", + "telemetryWorkspace": "mission-core-compute-module-adaptive-window-v3", + "telemetrySurface": "borderless-soft-surface-v1", + "telemetryStatus": "mission-core-dot-status-v1", + "telemetryNavigation": "full-workspace-back-navigation-v1", + "telemetryScroll": "reset-on-workspace-transition-v1", + "telemetryPollInterval": "three-seconds", + "telemetryFreshness": "fifteen-seconds-missing-stale-not-unhealthy", + "telemetryOntologyProjection": "observation-observed-property-provenance-freshness-v1", + "telemetryAgent": "telegraf-host-observer-v1", + "telemetryGraphScale": "adaptive-observed-window-explicit-domain-v1", + "telemetryCpuMinimumSpan": "five-percentage-points", + "telemetryMemoryMinimumSpan": "four-percentage-points", + "telemetryNetworkMissingSemantics": "missing-counters-never-zero-v1", + "interactiveShell": "disabled-pending-managed-session-boundary", + "gelios": "untouched-legacy-only", + "rollback": "restore-preapply-snapshot-preserve-manager-data" +} diff --git a/infra/deploy-runner/build-device-control-core-release-artifact.mjs b/infra/deploy-runner/build-device-control-core-release-artifact.mjs index 170e027..de93e3b 100644 --- a/infra/deploy-runner/build-device-control-core-release-artifact.mjs +++ b/infra/deploy-runner/build-device-control-core-release-artifact.mjs @@ -16,14 +16,16 @@ const [ predecessorSha256, ...extra ] = process.argv.slice(2); +const isV4 = patchId.startsWith("device-control-core-release-v4-"); if ( extra.length - || !/^device-control-core-release-[A-Za-z0-9._-]{1,67}$/.test(patchId) + || !/^device-control-core-release(?:-v[234])?-[A-Za-z0-9._-]{1,67}$/.test(patchId) + || (isV4 && patchId !== "device-control-core-release-v4-20260823-047") || ((predecessorPatchId === undefined) !== (predecessorSha256 === undefined)) || ( predecessorPatchId !== undefined && ( - !/^device-control-core-release-[A-Za-z0-9._-]{1,67}$/.test(predecessorPatchId) + !/^(?:device-control-core-release(?:-v[234])?-[A-Za-z0-9._-]{1,67}|device-control-core-migration-replay-checkpoint-recovery-20260822-046)$/.test(predecessorPatchId) || predecessorPatchId === patchId || !/^[0-9a-f]{64}$/.test(predecessorSha256) ) @@ -37,12 +39,13 @@ if ( const isV3 = patchId.startsWith("device-control-core-release-v3-"); const isV2 = patchId.startsWith("device-control-core-release-v2-"); +const includesTelemetry = isV3 || isV4; const coreDockerfile = await readFile( resolve(devicePlaneRoot, "services/device-control-core/Dockerfile"), "utf8", ); if ( - !isV3 + !includesTelemetry && coreDockerfile.includes( "COPY packages/infrastructure-telemetry-contract ./packages/infrastructure-telemetry-contract", ) @@ -53,7 +56,23 @@ const expectedV2Predecessor = Object.freeze({ patchId: predecessorPatchId ?? "device-control-core-release-20260812-024", artifactSha256: predecessorSha256 ?? "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793", }); -const descriptorPath = isV3 +const expectedV4Predecessor = Object.freeze({ + patchId: "device-control-core-migration-replay-checkpoint-recovery-20260822-046", + artifactSha256: "46000c76977fb583fc7c9cf74ecf624efd8b404f7b8d0322e0270e7b8ac6e450", +}); +if ( + isV4 + && predecessorPatchId !== undefined + && ( + predecessorPatchId !== expectedV4Predecessor.patchId + || predecessorSha256 !== expectedV4Predecessor.artifactSha256 + ) +) { + throw new Error("device_control_core_release_v4_predecessor_mismatch"); +} +const descriptorPath = isV4 + ? "deployment/device-control-core-release-v4.json" + : isV3 ? "deployment/device-control-core-release-v3.json" : isV2 ? "deployment/device-control-core-release-v2.json" @@ -64,7 +83,7 @@ const entries = [ "package-lock.json", "packages/device-protocol-contract", "packages/device-edge-channel-contract", - ...(isV3 ? ["packages/infrastructure-telemetry-contract"] : []), + ...(includesTelemetry ? ["packages/infrastructure-telemetry-contract"] : []), "services/device-control-core", descriptorPath, ]; @@ -83,7 +102,9 @@ try { descriptor.releaseId = patchId; if (predecessorPatchId !== undefined) { descriptor.predecessor = { - kind: "release", + kind: isV4 + ? "migration-replay-checkpoint-recovery" + : "release", patchId: predecessorPatchId, artifactSha256: predecessorSha256, }; @@ -104,7 +125,7 @@ try { "services/device-control-core/src/device-gateway-core-runtime.mjs", "packages/device-protocol-contract/src/index.mjs", "packages/device-edge-channel-contract/src/index.mjs", - ...(isV3 ? ["packages/infrastructure-telemetry-contract/src/index.mjs"] : []), + ...(includesTelemetry ? ["packages/infrastructure-telemetry-contract/src/index.mjs"] : []), ]) { const imported = spawnSync( process.execPath, @@ -118,7 +139,7 @@ try { const descriptor = JSON.parse(await readFile(join(payload, descriptorPath), "utf8")); if ( - descriptor.schemaVersion !== `nodedc.device-plane.device-control-core-release.${isV3 ? "v3" : isV2 ? "v2" : "v1"}` + descriptor.schemaVersion !== `nodedc.device-plane.device-control-core-release.${isV4 ? "v4" : isV3 ? "v3" : isV2 ? "v2" : "v1"}` || descriptor.releaseId !== patchId || descriptor.action !== "upgrade" || descriptor.service !== "device-control-core" @@ -130,20 +151,20 @@ try { || JSON.stringify(descriptor.coreNetworks) !== JSON.stringify(["device-plane-private", "device-plane-egress"]) || descriptor.publicIngress !== "none-on-synology" || descriptor.edgeRegistrations !== "preserved" - || descriptor.commandTransport !== ((isV2 || isV3) ? "typed-service-ping-v1" : "disabled") - || descriptor.gelios !== ((isV2 || isV3) ? "untouched-legacy-only" : "untouched") + || descriptor.commandTransport !== ((isV2 || isV3 || isV4) ? "typed-service-ping-v1" : "disabled") + || descriptor.gelios !== ((isV2 || isV3 || isV4) ? "untouched-legacy-only" : "untouched") || descriptor.rollback !== "restore-preapply-source-and-core-runtime" || ( - (isV2 || isV3) + (isV2 || isV3 || isV4) && ( descriptor.commandCatalog !== "allowlisted-adapter-typed-commands-only" || descriptor.credentialBoundary !== "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned" - || descriptor.predecessor?.patchId !== expectedV2Predecessor.patchId - || descriptor.predecessor?.artifactSha256 !== expectedV2Predecessor.artifactSha256 + || descriptor.predecessor?.patchId !== (isV4 ? expectedV4Predecessor : expectedV2Predecessor).patchId + || descriptor.predecessor?.artifactSha256 !== (isV4 ? expectedV4Predecessor : expectedV2Predecessor).artifactSha256 ) ) || ( - isV3 + includesTelemetry && ( descriptor.telemetryTransport !== "edge-channel-host-telemetry-observed-v1" || descriptor.telemetryContract !== "nodedc.infrastructure.host-telemetry.v1" @@ -152,6 +173,17 @@ try { || descriptor.telemetryFreshness !== "fifteen-seconds-missing-stale-not-unhealthy" ) ) + || ( + isV4 + && ( + descriptor.predecessor?.kind !== "migration-replay-checkpoint-recovery" + || descriptor.recoveryPredecessor !== "terminal-applied-046-exact-source-runtime-database" + || descriptor.databasePreflight !== "final-migration-016-validated-and-host-telemetry-table-absent" + || descriptor.databaseRowMutation !== "none-before-core-startup-migrations" + || descriptor.databaseSchemaOutcome !== "migration-017-host-telemetry-table-present" + || descriptor.runtimePredecessor !== "healthy-recovery-046-core-generation" + ) + ) ) { throw new Error("device_control_core_release_contract_mismatch"); } diff --git a/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs b/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs index 688d5c0..de10e88 100644 --- a/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs +++ b/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs @@ -11,11 +11,19 @@ const platformRoot = resolve(scriptDir, "../.."); const devicePlaneRoot = platformRoot; const managerRoot = resolve(platformRoot, "apps/device-manager"); const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts")); -const [patchId = "device-manager-release-v9-20260822-041", ...extra] = process.argv.slice(2); +const [patchId = "device-manager-release-v13-20260823-051", ...extra] = process.argv.slice(2); if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) throw new Error("usage: build-device-manager-control-plane-artifact.mjs [patch-id]"); -const descriptorPath = patchId.startsWith("device-manager-release-v9-") - ? "deployment/device-manager-release-v9.json" +const descriptorPath = patchId.startsWith("device-manager-release-v13-") + ? "deployment/device-manager-release-v13.json" + : patchId.startsWith("device-manager-release-v12-") + ? "deployment/device-manager-release-v12.json" + : patchId.startsWith("device-manager-release-v11-") + ? "deployment/device-manager-release-v11.json" + : patchId.startsWith("device-manager-release-v10-") + ? "deployment/device-manager-release-v10.json" + : patchId.startsWith("device-manager-release-v9-") + ? "deployment/device-manager-release-v9.json" : patchId.startsWith("device-manager-release-v8-") ? "deployment/device-manager-release-v8.json" : patchId.startsWith("device-manager-release-v7-") @@ -37,7 +45,11 @@ const isV6 = descriptorPath.endsWith("release-v6.json"); const isV7 = descriptorPath.endsWith("release-v7.json"); const isV8 = descriptorPath.endsWith("release-v8.json"); const isV9 = descriptorPath.endsWith("release-v9.json"); -const isPersistent = isV4 || isV5 || isV6 || isV7 || isV8 || isV9; +const isV10 = descriptorPath.endsWith("release-v10.json"); +const isV11 = descriptorPath.endsWith("release-v11.json"); +const isV12 = descriptorPath.endsWith("release-v12.json"); +const isV13 = descriptorPath.endsWith("release-v13.json"); +const isPersistent = isV4 || isV5 || isV6 || isV7 || isV8 || isV9 || isV10 || isV11 || isV12 || isV13; const isManagerOnly = isV3 || isPersistent; const composeSource = resolve(devicePlaneRoot, "docker-compose.device-manager.yml"); const composeSourceSha256 = createHash("sha256").update(await readFile(composeSource)).digest("hex"); @@ -176,7 +188,127 @@ try { : "restore-preapply-snapshot") ); if (commonContractInvalid) throw new Error("device_manager_activation_successor_contract_mismatch"); - if (descriptorPath.endsWith("release-v9.json")) { + if (descriptorPath.endsWith("release-v13.json")) { + if ( + descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v13" + || descriptor.predecessor?.kind !== "release" + || descriptor.predecessor?.patchId !== "device-manager-release-v12-20260823-050" + || descriptor.predecessor?.artifactSha256 !== "1a49839140e5f2e49763d78f24ee47d946e244bcfde15a9c38266e8bd14c0d49" + || descriptor.controlCorePredecessor?.patchId !== "device-control-core-release-v4-20260823-047" + || descriptor.controlCorePredecessor?.artifactSha256 !== "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + || descriptor.edgeChannelPredecessor?.patchId !== "device-edge-core-channel-upgrade-v4-20260812-023" + || descriptor.edgeChannelPredecessor?.artifactSha256 !== "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" + || descriptor.designSystem !== "nodedc-canonical-components-and-tokens-v1" + || descriptor.missionCoreReference !== "compute-modules-workspace-71c8b04" + || descriptor.infrastructureWorkspaceLayout !== "mission-core-system-workspace-v2" + || descriptor.hostInventoryComposition !== "mission-core-compute-host-accordion-v2" + || descriptor.hostInventoryOverviewSurface !== "separate-summary-soft-surface-v1" + || descriptor.hostInventoryCollectionSurface !== "separate-host-collection-soft-surface-v1" + || descriptor.hostInventoryRow !== "compact-centered-accordion-v1" + || descriptor.hostInventoryFreshness !== "dot-only-v1" + || descriptor.hostInventoryRelations !== "host-scoped-endpoint-deployment-service-v1" + || descriptor.hostInventoryDefaultExpansion !== "collapsed" + || descriptor.hostInventoryScaleTarget !== "five-hundred-collapsed-rows-v1" + || descriptor.telemetryWorkspace !== "mission-core-compute-module-adaptive-window-v3" + || descriptor.telemetrySurface !== "borderless-soft-surface-v1" + || descriptor.telemetryStatus !== "mission-core-dot-status-v1" + || descriptor.telemetryNavigation !== "full-workspace-back-navigation-v1" + || descriptor.telemetryScroll !== "reset-on-workspace-transition-v1" + || descriptor.telemetryPollInterval !== "three-seconds" + || descriptor.telemetryFreshness !== "fifteen-seconds-missing-stale-not-unhealthy" + || descriptor.telemetryOntologyProjection !== "observation-observed-property-provenance-freshness-v1" + || descriptor.telemetryAgent !== "telegraf-host-observer-v1" + || descriptor.telemetryGraphScale !== "adaptive-observed-window-explicit-domain-v1" + || descriptor.telemetryCpuMinimumSpan !== "five-percentage-points" + || descriptor.telemetryMemoryMinimumSpan !== "four-percentage-points" + || descriptor.telemetryNetworkMissingSemantics !== "missing-counters-never-zero-v1" + || descriptor.interactiveShell !== "disabled-pending-managed-session-boundary" + || descriptor.rollback !== "restore-preapply-snapshot-preserve-manager-data" + || descriptor.gelios !== "untouched-legacy-only" + ) throw new Error("device_manager_v13_host_inventory_accordion_contract_mismatch"); + await validateFaviconBundle(payload, "device_manager_v13"); + } else if (descriptorPath.endsWith("release-v12.json")) { + if ( + descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v12" + || descriptor.predecessor?.kind !== "release" + || descriptor.predecessor?.patchId !== "device-manager-release-v11-20260823-049" + || descriptor.predecessor?.artifactSha256 !== "c1e2056b50bfbb0d03d077461d0c27cc56cc52967c3f5620be14871c8a6d5cf0" + || descriptor.controlCorePredecessor?.patchId !== "device-control-core-release-v4-20260823-047" + || descriptor.controlCorePredecessor?.artifactSha256 !== "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + || descriptor.edgeChannelPredecessor?.patchId !== "device-edge-core-channel-upgrade-v4-20260812-023" + || descriptor.edgeChannelPredecessor?.artifactSha256 !== "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" + || descriptor.designSystem !== "nodedc-canonical-components-and-tokens-v1" + || descriptor.missionCoreReference !== "compute-modules-workspace-71c8b04" + || descriptor.infrastructureWorkspaceLayout !== "mission-core-system-workspace-v1" + || descriptor.hostInventoryComposition !== "mission-core-compute-host-list-v1" + || descriptor.telemetryWorkspace !== "mission-core-compute-module-adaptive-window-v3" + || descriptor.telemetrySurface !== "borderless-soft-surface-v1" + || descriptor.telemetryStatus !== "mission-core-dot-status-v1" + || descriptor.telemetryNavigation !== "full-workspace-back-navigation-v1" + || descriptor.telemetryScroll !== "reset-on-workspace-transition-v1" + || descriptor.telemetryPollInterval !== "three-seconds" + || descriptor.telemetryFreshness !== "fifteen-seconds-missing-stale-not-unhealthy" + || descriptor.telemetryOntologyProjection !== "observation-observed-property-provenance-freshness-v1" + || descriptor.telemetryAgent !== "telegraf-host-observer-v1" + || descriptor.telemetryGraphScale !== "adaptive-observed-window-explicit-domain-v1" + || descriptor.telemetryCpuMinimumSpan !== "five-percentage-points" + || descriptor.telemetryMemoryMinimumSpan !== "four-percentage-points" + || descriptor.telemetryNetworkMissingSemantics !== "missing-counters-never-zero-v1" + || descriptor.interactiveShell !== "disabled-pending-managed-session-boundary" + || descriptor.rollback !== "restore-preapply-snapshot-preserve-manager-data" + || descriptor.gelios !== "untouched-legacy-only" + ) throw new Error("device_manager_v12_adaptive_telemetry_graph_contract_mismatch"); + await validateFaviconBundle(payload, "device_manager_v12"); + } else if (descriptorPath.endsWith("release-v11.json")) { + if ( + descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v11" + || descriptor.predecessor?.kind !== "release" + || descriptor.predecessor?.patchId !== "device-manager-release-v10-20260823-048" + || descriptor.predecessor?.artifactSha256 !== "e6b983a314db4f8c27d89062dfedf5ed0523cc30421170799d181a19e2d85d4c" + || descriptor.controlCorePredecessor?.patchId !== "device-control-core-release-v4-20260823-047" + || descriptor.controlCorePredecessor?.artifactSha256 !== "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + || descriptor.edgeChannelPredecessor?.patchId !== "device-edge-core-channel-upgrade-v4-20260812-023" + || descriptor.edgeChannelPredecessor?.artifactSha256 !== "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" + || descriptor.designSystem !== "nodedc-canonical-components-and-tokens-v1" + || descriptor.missionCoreReference !== "compute-modules-workspace-71c8b04" + || descriptor.infrastructureWorkspaceLayout !== "mission-core-system-workspace-v1" + || descriptor.hostInventoryComposition !== "mission-core-compute-host-list-v1" + || descriptor.telemetryWorkspace !== "mission-core-compute-module-visual-parity-v2" + || descriptor.telemetrySurface !== "borderless-soft-surface-v1" + || descriptor.telemetryStatus !== "mission-core-dot-status-v1" + || descriptor.telemetryNavigation !== "full-workspace-back-navigation-v1" + || descriptor.telemetryScroll !== "reset-on-workspace-transition-v1" + || descriptor.telemetryPollInterval !== "three-seconds" + || descriptor.telemetryFreshness !== "fifteen-seconds-missing-stale-not-unhealthy" + || descriptor.telemetryOntologyProjection !== "observation-observed-property-provenance-freshness-v1" + || descriptor.telemetryAgent !== "telegraf-host-observer-v1" + || descriptor.interactiveShell !== "disabled-pending-managed-session-boundary" + || descriptor.rollback !== "restore-preapply-snapshot-preserve-manager-data" + || descriptor.gelios !== "untouched-legacy-only" + ) throw new Error("device_manager_v11_mission_core_visual_parity_contract_mismatch"); + await validateFaviconBundle(payload, "device_manager_v11"); + } else if (descriptorPath.endsWith("release-v10.json")) { + if ( + descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v10" + || descriptor.predecessor?.kind !== "release" + || descriptor.predecessor?.patchId !== "device-manager-release-v8-20260822-039" + || descriptor.predecessor?.artifactSha256 !== "30a83d4c6b5c029c96c4af19fa558e0ae75e4b60bc897881457304bd17e0e465" + || descriptor.controlCorePredecessor?.patchId !== "device-control-core-release-v4-20260823-047" + || descriptor.controlCorePredecessor?.artifactSha256 !== "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + || descriptor.edgeChannelPredecessor?.patchId !== "device-edge-core-channel-upgrade-v4-20260812-023" + || descriptor.edgeChannelPredecessor?.artifactSha256 !== "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" + || descriptor.telemetryWorkspace !== "mission-core-compute-module-parity-v1" + || descriptor.telemetryNavigation !== "full-workspace-back-navigation-v1" + || descriptor.telemetryPollInterval !== "three-seconds" + || descriptor.telemetryFreshness !== "fifteen-seconds-missing-stale-not-unhealthy" + || descriptor.telemetryOntologyProjection !== "observation-observed-property-provenance-freshness-v1" + || descriptor.telemetryAgent !== "telegraf-host-observer-v1" + || descriptor.interactiveShell !== "disabled-pending-managed-session-boundary" + || descriptor.rollback !== "restore-preapply-snapshot-preserve-manager-data" + || descriptor.gelios !== "untouched-legacy-only" + ) throw new Error("device_manager_v10_host_telemetry_workspace_contract_mismatch"); + await validateFaviconBundle(payload, "device_manager_v10"); + } else if (descriptorPath.endsWith("release-v9.json")) { if ( descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v9" || descriptor.predecessor?.kind !== "release" diff --git a/infra/deploy-runner/nodedc-b2-vps-deploy b/infra/deploy-runner/nodedc-b2-vps-deploy index 9e7b0fa..1b50393 100755 --- a/infra/deploy-runner/nodedc-b2-vps-deploy +++ b/infra/deploy-runner/nodedc-b2-vps-deploy @@ -1104,10 +1104,17 @@ def ensure_service_user(name=SERVICE_USER, home_dir="/var/lib/nodedc-b2-vps"): def extract_vendor_binary(archive: Path, member_name: str, target: Path, mode=0o755): with tarfile.open(archive, "r:*") as package: - try: - member = package.getmember(member_name) - except KeyError: + accepted_names = {member_name, f"./{member_name}"} + matches = [ + candidate + for candidate in package.getmembers() + if candidate.name in accepted_names + ] + if not matches: die(f"vendor binary member missing: {member_name}") + if len(matches) != 1: + die(f"vendor binary member ambiguous: {member_name}") + member = matches[0] if not member.isfile() or member.issym() or member.islnk(): die(f"vendor binary member unsafe: {member_name}") source = package.extractfile(member) diff --git a/infra/deploy-runner/test_device_edge_vps_artifact.py b/infra/deploy-runner/test_device_edge_vps_artifact.py index 26a55fa..1f2646e 100644 --- a/infra/deploy-runner/test_device_edge_vps_artifact.py +++ b/infra/deploy-runner/test_device_edge_vps_artifact.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import hashlib +import io import importlib.machinery import importlib.util import json @@ -479,6 +480,52 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase): "c7486ec879681ddd706f229b628c8556ca8c9ccc4f152a85debb409c302759ef", ) + def test_vendor_binary_extractor_accepts_official_leading_dot_member(self): + member_name = "telegraf-1.38.4/usr/bin/telegraf" + binary = b"pinned-telegraf-binary" + with tempfile.TemporaryDirectory( + prefix="nodedc-vps-vendor-leading-dot-", + ) as directory: + root = Path(directory) + archive = root / "telegraf.tgz" + target = root / "runtime/telegraf" + with tarfile.open(archive, "w:gz") as package: + member = tarfile.TarInfo(f"./{member_name}") + member.size = len(binary) + package.addfile(member, io.BytesIO(binary)) + with patch.object(RUNNER.os, "chown"): + RUNNER.extract_vendor_binary( + archive, + member_name, + target, + ) + self.assertEqual(target.read_bytes(), binary) + self.assertEqual(target.stat().st_mode & 0o777, 0o755) + + def test_vendor_binary_extractor_rejects_ambiguous_spelling(self): + member_name = "telegraf-1.38.4/usr/bin/telegraf" + with tempfile.TemporaryDirectory( + prefix="nodedc-vps-vendor-ambiguous-", + ) as directory: + root = Path(directory) + archive = root / "telegraf.tgz" + target = root / "runtime/telegraf" + with tarfile.open(archive, "w:gz") as package: + for name in (member_name, f"./{member_name}"): + member = tarfile.TarInfo(name) + member.size = 1 + package.addfile(member, io.BytesIO(b"x")) + with self.assertRaisesRegex( + RUNNER.DeployError, + "vendor binary member ambiguous", + ): + RUNNER.extract_vendor_binary( + archive, + member_name, + target, + ) + self.assertFalse(target.exists()) + def test_publish_payload_preserves_unselected_executable_modes(self): with tempfile.TemporaryDirectory(prefix="nodedc-vps-publish-scope-") as directory: root = Path(directory) diff --git a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py index 5e16a6b..abf19cd 100644 --- a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py +++ b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py @@ -723,6 +723,259 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): ) ) + def test_device_manager_release_v10_pins_successful_telemetry_core(self): + patch_id = "device-manager-release-v10-unit-001" + manifest, entries, names, result = self.assert_deterministic_artifact( + "build-device-manager-control-plane-artifact.mjs", + patch_id, + RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V10_ENTRIES, + ) + self.assertEqual(manifest["component"], "device-plane") + self.assertEqual(result["services"], ["device-manager"]) + self.assertFalse( + any( + name.startswith("payload/services/device-control-core/") + for name in names + ) + ) + self.assertFalse(any(name.startswith("payload/packages/") for name in names)) + template = json.loads( + ( + DEVICE_CORE_ROOT + / "deployment/device-manager-release-v10.json" + ).read_text(encoding="utf-8") + ) + descriptor = {**template, "releaseId": patch_id} + self.assertIs( + RUNNER.validate_device_plane_manager_release_descriptor( + descriptor, + schema_version=( + "nodedc.device-plane.device-manager-release.v10" + ), + boundaries=( + RUNNER.expected_device_plane_manager_release_v10_boundaries() + ), + expected_release_id=patch_id, + ), + descriptor, + ) + self.assertEqual( + descriptor["predecessor"], + { + "kind": "release", + "patchId": "device-manager-release-v8-20260822-039", + "artifactSha256": ( + "30a83d4c6b5c029c96c4af19fa558e0ae75e4b60bc897881457304bd17e0e465" + ), + }, + ) + self.assertEqual( + descriptor["controlCorePredecessor"], + { + "patchId": "device-control-core-release-v4-20260823-047", + "artifactSha256": ( + "4aecceeb8d400fdd3dbec8fc86b151691f389d165e72677f396f8994823b6b5c" + ), + }, + ) + self.assertEqual( + descriptor["telemetryWorkspace"], + "mission-core-compute-module-parity-v1", + ) + self.assertTrue( + RUNNER.is_device_plane_manager_release_v10_slice( + "device-plane", + entries, + ) + ) + + def test_device_manager_release_v11_pins_v10_and_visual_parity(self): + patch_id = "device-manager-release-v11-unit-001" + manifest, entries, names, result = self.assert_deterministic_artifact( + "build-device-manager-control-plane-artifact.mjs", + patch_id, + RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V11_ENTRIES, + ) + self.assertEqual(manifest["component"], "device-plane") + self.assertEqual(result["services"], ["device-manager"]) + self.assertFalse( + any( + name.startswith("payload/services/device-control-core/") + for name in names + ) + ) + self.assertFalse(any(name.startswith("payload/packages/") for name in names)) + template = json.loads( + ( + DEVICE_CORE_ROOT + / "deployment/device-manager-release-v11.json" + ).read_text(encoding="utf-8") + ) + descriptor = {**template, "releaseId": patch_id} + self.assertIs( + RUNNER.validate_device_plane_manager_release_descriptor( + descriptor, + schema_version=( + "nodedc.device-plane.device-manager-release.v11" + ), + boundaries=( + RUNNER.expected_device_plane_manager_release_v11_boundaries() + ), + expected_release_id=patch_id, + ), + descriptor, + ) + self.assertEqual( + descriptor["predecessor"], + { + "kind": "release", + "patchId": "device-manager-release-v10-20260823-048", + "artifactSha256": ( + "e6b983a314db4f8c27d89062dfedf5ed0523cc30421170799d181a19e2d85d4c" + ), + }, + ) + self.assertEqual( + descriptor["telemetryWorkspace"], + "mission-core-compute-module-visual-parity-v2", + ) + self.assertEqual( + descriptor["telemetrySurface"], + "borderless-soft-surface-v1", + ) + self.assertTrue( + RUNNER.is_device_plane_manager_release_v11_slice( + "device-plane", + entries, + ) + ) + + def test_device_manager_release_v12_pins_v11_and_adaptive_graphs(self): + patch_id = "device-manager-release-v12-unit-001" + manifest, entries, names, result = self.assert_deterministic_artifact( + "build-device-manager-control-plane-artifact.mjs", + patch_id, + RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V12_ENTRIES, + ) + self.assertEqual(manifest["component"], "device-plane") + self.assertEqual(result["services"], ["device-manager"]) + self.assertFalse( + any( + name.startswith("payload/services/device-control-core/") + for name in names + ) + ) + self.assertFalse(any(name.startswith("payload/packages/") for name in names)) + template = json.loads( + ( + DEVICE_CORE_ROOT + / "deployment/device-manager-release-v12.json" + ).read_text(encoding="utf-8") + ) + descriptor = {**template, "releaseId": patch_id} + self.assertIs( + RUNNER.validate_device_plane_manager_release_descriptor( + descriptor, + schema_version=( + "nodedc.device-plane.device-manager-release.v12" + ), + boundaries=( + RUNNER.expected_device_plane_manager_release_v12_boundaries() + ), + expected_release_id=patch_id, + ), + descriptor, + ) + self.assertEqual( + descriptor["predecessor"], + { + "kind": "release", + "patchId": "device-manager-release-v11-20260823-049", + "artifactSha256": ( + "c1e2056b50bfbb0d03d077461d0c27cc56cc52967c3f5620be14871c8a6d5cf0" + ), + }, + ) + self.assertEqual( + descriptor["telemetryGraphScale"], + "adaptive-observed-window-explicit-domain-v1", + ) + self.assertEqual( + descriptor["telemetryNetworkMissingSemantics"], + "missing-counters-never-zero-v1", + ) + self.assertTrue( + RUNNER.is_device_plane_manager_release_v12_slice( + "device-plane", + entries, + ) + ) + + def test_device_manager_release_v13_pins_v12_and_host_accordion(self): + patch_id = "device-manager-release-v13-unit-001" + manifest, entries, names, result = self.assert_deterministic_artifact( + "build-device-manager-control-plane-artifact.mjs", + patch_id, + RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V13_ENTRIES, + ) + self.assertEqual(manifest["component"], "device-plane") + self.assertEqual(result["services"], ["device-manager"]) + self.assertFalse( + any( + name.startswith("payload/services/device-control-core/") + for name in names + ) + ) + self.assertFalse(any(name.startswith("payload/packages/") for name in names)) + template = json.loads( + ( + DEVICE_CORE_ROOT + / "deployment/device-manager-release-v13.json" + ).read_text(encoding="utf-8") + ) + descriptor = {**template, "releaseId": patch_id} + self.assertIs( + RUNNER.validate_device_plane_manager_release_descriptor( + descriptor, + schema_version=( + "nodedc.device-plane.device-manager-release.v13" + ), + boundaries=( + RUNNER.expected_device_plane_manager_release_v13_boundaries() + ), + expected_release_id=patch_id, + ), + descriptor, + ) + self.assertEqual( + descriptor["predecessor"], + { + "kind": "release", + "patchId": "device-manager-release-v12-20260823-050", + "artifactSha256": ( + "1a49839140e5f2e49763d78f24ee47d946e244bcfde15a9c38266e8bd14c0d49" + ), + }, + ) + self.assertEqual( + descriptor["hostInventoryRow"], + "compact-centered-accordion-v1", + ) + self.assertEqual( + descriptor["hostInventoryRelations"], + "host-scoped-endpoint-deployment-service-v1", + ) + self.assertEqual( + descriptor["telemetryWorkspace"], + "mission-core-compute-module-adaptive-window-v3", + ) + self.assertTrue( + RUNNER.is_device_plane_manager_release_v13_slice( + "device-plane", + entries, + ) + ) + def test_historical_manager_builder_fails_closed_after_v4_compose(self): if self.historical_manager_compose_is_current(): self.skipTest("historical Manager Compose is still current") @@ -1107,6 +1360,93 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): ) self.assertEqual(descriptor["gelios"], "untouched-legacy-only") + def test_control_core_release_v4_is_recovery_pinned_telemetry_core_only(self): + patch_id = "device-control-core-release-v4-20260823-047" + manifest, entries, names, result = self.assert_deterministic_artifact( + "build-device-control-core-release-artifact.mjs", + patch_id, + RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_ENTRIES, + ) + self.assertEqual(manifest["component"], "device-plane") + self.assertEqual(result["services"], ["device-control-core"]) + self.assertEqual( + RUNNER.component_services("device-plane", entries), + ("device-control-core",), + ) + self.assertIn( + "payload/deployment/device-control-core-release-v4.json", + names, + ) + self.assertIn( + "payload/packages/infrastructure-telemetry-contract/src/index.mjs", + names, + ) + self.assertIn( + "payload/services/device-control-core/migrations/" + "017_infrastructure_host_telemetry.sql", + names, + ) + self.assertFalse(any("docker-compose" in name for name in names)) + with tempfile.TemporaryDirectory( + prefix="nodedc-control-core-v4-read-", + ) as directory: + result = self.build( + "build-device-control-core-release-artifact.mjs", + patch_id, + Path(directory), + ) + extracted = Path(directory) / "extracted" + extracted.mkdir() + _manifest, _entries, payload = RUNNER.load_artifact( + Path(result["artifact"]), + extracted, + ) + descriptor = json.loads( + ( + payload / RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V4_REL + ).read_text(encoding="utf-8") + ) + self.assertEqual( + descriptor["predecessor"], + { + "kind": "migration-replay-checkpoint-recovery", + "patchId": ( + "device-control-core-migration-replay-checkpoint-" + "recovery-20260822-046" + ), + "artifactSha256": ( + "46000c76977fb583fc7c9cf74ecf624efd8b404f7b8d0322e0270e7b8ac6e450" + ), + }, + ) + self.assertEqual( + descriptor["databaseSchemaOutcome"], + "migration-017-host-telemetry-table-present", + ) + + def test_control_core_release_v4_rejects_any_other_identity(self): + with tempfile.TemporaryDirectory( + prefix="nodedc-control-core-v4-wrong-id-", + ) as directory: + environment = os.environ.copy() + environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = directory + completed = subprocess.run( + [ + "node", + str( + SCRIPT_DIR + / "build-device-control-core-release-artifact.mjs" + ), + "device-control-core-release-v4-20260823-999", + ], + cwd=DEVICE_CORE_ROOT, + env=environment, + capture_output=True, + text=True, + ) + self.assertNotEqual(completed.returncode, 0) + self.assertIn("usage:", completed.stderr) + def test_control_core_release_builder_supports_release_predecessor(self): if not self.historical_control_core_builders_are_current(): self.skipTest("historical Core release v1 builder generation is frozen")