Compare commits

...
4 Commits
18 changed files with 2827 additions and 445 deletions
@@ -84,6 +84,74 @@ test("Device Core overview follows the Mission Core landing-stage geometry", asy
assert.ok(!client.includes('<GlassSurface className="device-manager-home"'));
});
test("VPS inventory and monitoring follow the Mission Core system workspace geometry", async () => {
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<Set<string>>',
'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("<ResourceCard"));
assert.ok(!infrastructureSource.includes('title="Сервисы хостов"'));
assert.ok(!infrastructureSource.includes('title="Deployments и endpoints"'));
assert.ok(!infrastructureSource.includes("ceiling={100}"));
assert.ok(!infrastructureSource.includes("Канонический ontology catalog"));
assert.ok(!styles.includes(".host-telemetry-metric,"));
assert.ok(!styles.includes(".host-telemetry-facts"));
});
test("legacy single teaser migrates into the environment media playlist", () => {
const environment = normalizeEnvironmentPresentation({
defaultTeaser: {
+402 -238
View File
@@ -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<string | null>(null);
const [expandedHostRefs, setExpandedHostRefs] = useState<Set<string>>(() => new Set());
const inventoryRef = useRef<HTMLDivElement>(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 (
<HostTelemetryWorkspace
@@ -513,142 +527,152 @@ function HostsView({
);
}
return (
<ControlStack>
<ControlToolbar
copy={`Канонический ontology catalog ${topology.ontology.catalogHash}: Host, endpoint, deployment и service instance существуют отдельно. Edge — опциональная роль service instance; credentials остаются server-side.`}
actions={canManageInfrastructure ? <>
<Button size="compact" onClick={onCreateHost}>Новый VPS</Button>
<Button size="compact" onClick={onCreateEndpoint} disabled={!topology.hosts.length}>Endpoint</Button>
<Button size="compact" onClick={onCreateDeployment} disabled={!topology.hosts.length}>Deployment</Button>
<Button size="compact" variant="primary" onClick={onCreateService} disabled={!topology.deployments.length}>Service</Button>
</> : null}
/>
<ControlSection title="VPS и хосты" count={topology.hosts.length}>
<ResourceGrid empty="VPS и хосты для проекта пока не зарегистрированы.">
{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 (
<ResourceCard
key={host.hostRef}
eyebrow="INFRASTRUCTURE / HOST"
title={host.displayName}
description={host.externalRef || host.hostKey}
status={host.health.state}
meta={[
`lifecycle · ${host.lifecycleState}`,
`health · ${host.health.freshness}`,
...(host.providerRef ? [`provider · ${host.providerRef}`] : []),
`${hostEndpoints.length} endpoints · ${hostServices.length} services`,
`management · ${host.managementCredentialConfigured ? "configured" : "unconfigured"}`,
]}
action={<>
<Button size="compact" variant="primary" onClick={() => setSelectedHostRef(host.hostRef)}>Мониторинг</Button>
{canManageInfrastructure ? (
<Button size="compact" onClick={onRecordHealth}>Health evidence</Button>
<div className="infrastructure-system-workspace" ref={inventoryRef}>
<section className="infrastructure-overview-block">
<div className="infrastructure-workspace-lead">
<div>
<span className="infrastructure-eyebrow">ИНФРАСТРУКТУРА / VPS И ХОСТЫ</span>
<h2>VPS и хосты</h2>
<p>Вычислительные узлы проекта, их подключения и запущенные сервисы.</p>
</div>
{canManageInfrastructure ? (
<div className="infrastructure-workspace-actions">
<Button size="compact" variant="primary" onClick={onCreateHost}>Новый VPS</Button>
</div>
) : null}
</div>
<div className="infrastructure-overview-grid" aria-label="Сводка инфраструктуры">
<InfrastructureCount label="ХОСТЫ" value={topology.hosts.length} detail={`${topology.hosts.filter((host) => host.telemetry.freshness === "fresh").length} со свежими данными`} />
<InfrastructureCount label="ENDPOINTS" value={topology.endpoints.length} detail="точки подключения" />
<InfrastructureCount label="DEPLOYMENTS" value={topology.deployments.length} detail="развёрнутые контуры" />
<InfrastructureCount label="SERVICES" value={topology.serviceInstances.length} detail="экземпляры сервисов" />
</div>
</section>
<section className="infrastructure-section infrastructure-hosts-block">
<InfrastructureSectionHeading
eyebrow="ВЫЧИСЛИТЕЛЬНЫЕ УЗЛЫ"
title="Зарегистрированные хосты"
description="Компактный список VPS. Раскройте только тот хост, связи которого нужно посмотреть."
status={russianCount(topology.hosts.length, "хост", "хоста", "хостов")}
actions={canManageInfrastructure ? <>
<Button size="compact" onClick={onRecordHealth}>Наблюдение</Button>
<Button size="compact" onClick={onCreateEndpoint} disabled={!topology.hosts.length}>Endpoint</Button>
<Button size="compact" onClick={onCreateDeployment} disabled={!topology.hosts.length}>Deployment</Button>
<Button size="compact" variant="primary" onClick={onCreateService} disabled={!topology.deployments.length}>Service</Button>
</> : null}
/>
{topology.hosts.length ? (
<div className="infrastructure-host-list">
{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 (
<article className="infrastructure-host-card" data-expanded={expanded ? "true" : undefined} key={host.hostRef}>
<header className="infrastructure-host-card__summary">
<div className="infrastructure-host-card__identity">
<span className="infrastructure-eyebrow">COMPUTE HOST</span>
<h4>{host.displayName}</h4>
<code>{host.externalRef || host.hostKey}</code>
</div>
<div className="infrastructure-host-card__actions">
<Button size="compact" variant="primary" onClick={() => setSelectedHostRef(host.hostRef)}>Мониторинг</Button>
<span
className="infrastructure-host-card__freshness"
data-freshness={host.telemetry.freshness}
role="status"
aria-label={freshnessLabel(host.telemetry.freshness)}
title={freshnessLabel(host.telemetry.freshness)}
/>
<IconButton
className="infrastructure-host-card__toggle"
label={expanded ? `Свернуть ${host.displayName}` : `Развернуть ${host.displayName}`}
aria-expanded={expanded}
aria-controls={detailsId}
onClick={() => toggleHost(host.hostRef)}
>
<Icon name="chevron-down" size={18} />
</IconButton>
</div>
</header>
{expanded ? (
<div className="infrastructure-host-card__details" id={detailsId}>
<dl className="infrastructure-host-card__facts">
<div><dt>Состояние</dt><dd>{host.lifecycleState}</dd></div>
<div><dt>Провайдер</dt><dd>{host.providerRef || "Не указан"}</dd></div>
<div><dt>Доступ управления</dt><dd>{host.managementCredentialConfigured ? "Настроен" : "Не настроен"}</dd></div>
<div><dt>Последнее наблюдение</dt><dd>{formatDate(host.telemetry.observedAt)}</dd></div>
</dl>
<div className="infrastructure-host-relations">
<section>
<header><span className="infrastructure-eyebrow">ENDPOINTS</span><strong>{hostEndpoints.length}</strong></header>
{hostEndpoints.length ? <div className="infrastructure-registry-list">{hostEndpoints.map((endpoint) => (
<InfrastructureRegistryRow key={endpoint.endpointRef} label={endpoint.purpose} title={endpoint.endpointKey} description={endpoint.endpointUri} status={endpoint.lifecycleState} />
))}</div> : <p>Точки подключения не зарегистрированы.</p>}
</section>
<section>
<header><span className="infrastructure-eyebrow">DEPLOYMENTS</span><strong>{hostDeployments.length}</strong></header>
{hostDeployments.length ? <div className="infrastructure-registry-list">{hostDeployments.map((deployment) => (
<InfrastructureRegistryRow key={deployment.deploymentRef} label="DEPLOYMENT" title={deployment.displayName} description={`${deployment.artifactRef} · ${shortDigest(deployment.artifactDigest)}`} status={deployment.lifecycleState} />
))}</div> : <p>Развёртывания не зарегистрированы.</p>}
</section>
<section>
<header><span className="infrastructure-eyebrow">SERVICES</span><strong>{hostServices.length}</strong></header>
{hostServices.length ? <div className="infrastructure-registry-list">{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 <InfrastructureRegistryRow key={service.serviceInstanceRef} label={service.serviceRole} title={service.displayName} description={`${service.serviceKey} · ${edge?.displayName || "Edge не связан"}`} status={runtimeState} />;
})}</div> : <p>Сервисы не зарегистрированы.</p>}
</section>
</div>
</div>
) : null}
</>}
/>
);
})}
</ResourceGrid>
</ControlSection>
<ControlSection title="Service instances" count={topology.serviceInstances.length}>
<ResourceGrid empty="Service instances ещё не связаны с deployments.">
{topology.serviceInstances.map((service) => {
const edge = service.edgeRef
? workspace.edges.find((item) => item.edgeRef === service.edgeRef)
: null;
return (
<ResourceCard
key={service.serviceInstanceRef}
eyebrow={service.serviceRole}
title={service.displayName}
description={service.serviceKey}
status={edge?.channel.runtimeState || service.health.state}
meta={[
`service · ${service.lifecycleState}`,
`health · ${service.health.freshness}`,
...(edge ? [
`Edge · ${edge.displayName}`,
`Core↔Edge · ${edge.channel.runtimeState}`,
] : []),
service.deploymentRef,
]}
/>
);
})}
</ResourceGrid>
</ControlSection>
<ControlSection title="Deployments и endpoints" count={topology.deployments.length + topology.endpoints.length}>
<ResourceList empty="Deployments и endpoints отсутствуют.">
{topology.deployments.map((deployment) => (
<ResourceRow
key={deployment.deploymentRef}
title={deployment.displayName}
description={`${deployment.artifactRef} · ${shortDigest(deployment.artifactDigest)}`}
status={deployment.lifecycleState}
trailing="deployment"
/>
))}
{topology.endpoints.map((endpoint) => (
<ResourceRow
key={endpoint.endpointRef}
title={endpoint.endpointKey}
description={endpoint.endpointUri}
status={endpoint.lifecycleState}
trailing={endpoint.purpose}
/>
))}
</ResourceList>
</ControlSection>
<ControlToolbar
copy="Asset — стабильный трайк или другой объект. B2 остаётся Device и связывается с Asset временным binding; замена трекера не меняет историю Asset."
actions={<>
{canManageAssets ? <Button size="compact" onClick={onCreateAsset}>Новый Asset</Button> : null}
{canManageBindings ? <Button size="compact" variant="primary" onClick={onCreateAssetBinding} disabled={!topology.assets.length || !workspace.devices.length}>Привязать tracker</Button> : null}
</>}
/>
<ControlSection title="Assets" count={topology.assets.length}>
<ResourceGrid empty="Assets проекта пока не созданы.">
{topology.assets.map((asset) => {
const activeBindings = topology.assetBindings.filter(
(binding) => binding.assetRef === asset.assetRef && !binding.validTo,
);
return (
<ResourceCard
key={asset.assetRef}
eyebrow="ASSET / STABLE IDENTITY"
title={asset.displayName}
description={asset.assetTypeRef}
status={asset.lifecycleState}
meta={[asset.assetKey, `${activeBindings.length} active device bindings`]}
/>
);
})}
</ResourceGrid>
</ControlSection>
<ControlSection title="Device ↔ Asset history" count={topology.assetBindings.length}>
<ResourceList empty="Tracker bindings отсутствуют.">
{topology.assetBindings.map((binding) => (
<ResourceRow
key={binding.assetBindingRef}
title={`${binding.deviceName} → ${binding.assetName}`}
description={`${binding.bindingKind} · ${binding.provenanceRef} · ${formatDate(binding.validFrom)}`}
status={binding.validTo ? "closed" : "active"}
trailing={!binding.validTo && canManageBindings ? (
<Button size="compact" onClick={() => onCloseAssetBinding(binding)}>Закрыть</Button>
) : formatDate(binding.validTo)}
/>
))}
</ResourceList>
</ControlSection>
<GlassSurface padding="md" tone="soft">
<p className="device-manager-card-copy">
Отсутствующее или просроченное health evidence отображается как unobserved, а не unreachable. Arbitrary WebSSH console отключена; будущая консоль потребует отдельной короткоживущей management session и break-glass аудита.
</p>
</GlassSurface>
</ControlStack>
</article>
);
})}
</div>
) : <div className="infrastructure-empty">VPS и хосты для проекта пока не зарегистрированы.</div>}
</section>
<section className="infrastructure-section infrastructure-assets-block">
<InfrastructureSectionHeading
eyebrow="DEVICE ASSETS"
title="Объекты и трекеры"
description="Стабильные объекты проекта и история привязанных к ним устройств."
status={russianCount(topology.assets.length, "объект", "объекта", "объектов")}
actions={<>
{canManageAssets ? <Button size="compact" onClick={onCreateAsset}>Новый Asset</Button> : null}
{canManageBindings ? <Button size="compact" variant="primary" onClick={onCreateAssetBinding} disabled={!topology.assets.length || !workspace.devices.length}>Привязать tracker</Button> : null}
</>}
/>
{topology.assets.length ? (
<div className="infrastructure-asset-grid">
{topology.assets.map((asset) => {
const activeBindings = topology.assetBindings.filter((binding) => binding.assetRef === asset.assetRef && !binding.validTo);
return (
<GlassSurface className="infrastructure-asset-card" padding="md" tone="soft" key={asset.assetRef}>
<div><span className="infrastructure-eyebrow">{asset.assetTypeRef}</span><strong>{asset.displayName}</strong><small>{asset.assetKey}</small></div>
<div><StatusBadge tone={statusTone(asset.lifecycleState)}>{asset.lifecycleState}</StatusBadge><small>{activeBindings.length} активных привязок</small></div>
</GlassSurface>
);
})}
</div>
) : <div className="infrastructure-empty">Объекты проекта пока не созданы.</div>}
{topology.assetBindings.length ? (
<div className="infrastructure-registry-list">
{topology.assetBindings.map((binding) => (
<div className="infrastructure-registry-row" key={binding.assetBindingRef}>
<div><span className="infrastructure-eyebrow">DEVICE ↔ ASSET</span><strong>{binding.deviceName} → {binding.assetName}</strong><small>{binding.bindingKind} · {formatDate(binding.validFrom)}</small></div>
<div><StatusBadge tone={binding.validTo ? "neutral" : "success"}>{binding.validTo ? "Закрыта" : "Активна"}</StatusBadge>{!binding.validTo && canManageBindings ? <Button size="compact" onClick={() => onCloseAssetBinding(binding)}>Закрыть</Button> : null}</div>
</div>
))}
</div>
) : null}
</section>
</div>
);
}
@@ -665,6 +689,12 @@ function HostTelemetryWorkspace({
onPoll: () => Promise<void>;
onError: (reason: unknown) => void;
}) {
const workspaceRef = useRef<HTMLDivElement>(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 (
<div className="host-telemetry-workspace">
<header className="host-telemetry-header">
<IconButton label="Вернуться к VPS и хостам" onClick={onBack}>
<Icon name="chevron-left" size={18} />
</IconButton>
<div className="host-telemetry-header__copy">
<small>СИСТЕМА / ВЫЧИСЛИТЕЛЬНЫЕ МОДУЛИ</small>
<div className="infrastructure-system-workspace host-monitoring-workspace" ref={workspaceRef}>
<section className="infrastructure-workspace-lead">
<div>
<span className="infrastructure-eyebrow">СИСТЕМА / ВЫЧИСЛИТЕЛЬНЫЕ МОДУЛИ</span>
<h2>{host.displayName}</h2>
<p>Аппаратный и процессинговый срез VPS. Метрики снимает host-agent; Device Core хранит только канонические наблюдения.</p>
<p>Аппаратный и процессинговый срез выбранного VPS. Последнее обновление: {formatDate(telemetry.observedAt)}.</p>
</div>
<div className="host-telemetry-header__actions">
<StatusBadge tone={telemetry.freshness === "fresh" ? "success" : telemetry.freshness === "stale" ? "warning" : undefined}>
{telemetry.freshness === "fresh" ? "Свежие данные" : telemetry.freshness === "stale" ? "Данные устарели" : "Нет данных"}
</StatusBadge>
<div className="infrastructure-workspace-actions">
<StatusBadge tone={freshnessTone(telemetry.freshness)}>{freshnessLabel(telemetry.freshness)}</StatusBadge>
<IconButton label="Обновить телеметрию" onClick={() => onPoll().catch(onError)}>
<Icon name="refresh" size={17} />
</IconButton>
</div>
</header>
<section className="host-telemetry-metric-grid" aria-label="Ключевые метрики VPS">
<TelemetryMetricCard label="CPU" value={formatPercent(current?.cpu.usagePercent)} points={cpuHistory} detail={formatLoad(current?.cpu)} />
<TelemetryMetricCard label="RAM" value={formatPercent(current?.memory.usedPercent)} points={memoryHistory} detail={formatUsedTotal(current?.memory.usedBytes, current?.memory.totalBytes)} />
<TelemetryMetricCard label="NETWORK RX" value={formatRate(networkRate.received)} points={receiveHistory} detail="входящий трафик" />
<TelemetryMetricCard label="NETWORK TX" value={formatRate(networkRate.sent)} points={sendHistory} detail="исходящий трафик" />
</section>
<section className="host-telemetry-section">
<div className="host-telemetry-section__heading">
<div><small>HARDWARE</small><h3>{current?.hardware.hostname ?? host.hostKey}</h3></div>
<StatusBadge tone={telemetry.freshness === "fresh" ? "success" : "warning"}>{telemetry.state}</StatusBadge>
</div>
<div className="host-telemetry-facts">
<TelemetryFact label="Процессор" value={current?.hardware.cpuModel} />
<TelemetryFact label="Логические ядра" value={formatNullable(current?.hardware.logicalProcessors)} />
<TelemetryFact label="Память занята" value={formatUsedTotal(current?.memory.usedBytes, current?.memory.totalBytes)} />
<TelemetryFact label="Uptime" value={formatDuration(current?.system.uptimeSeconds)} />
<TelemetryFact label="Платформа" value={[current?.hardware.platform, current?.hardware.architecture].filter(Boolean).join(" / ") || null} />
<TelemetryFact label="Kernel" value={current?.hardware.kernelRelease} />
<TelemetryFact label="Процессы" value={formatNullable(current?.system.processes.total)} />
<TelemetryFact label="Load average" value={formatLoad(current?.cpu)} />
<IconButton label="Вернуться к VPS и хостам" onClick={onBack}>
<Icon name="chevron-left" size={18} />
</IconButton>
</div>
</section>
<div className="host-telemetry-split">
<section className="host-telemetry-section">
<div className="host-telemetry-section__heading"><div><small>STORAGE</small><h3>Файловые системы</h3></div><StatusBadge>{current?.disks.length ?? 0}</StatusBadge></div>
<div className="host-telemetry-list">
{(current?.disks ?? []).map((disk, index) => (
<div className="host-telemetry-list__row" key={`${disk.device}:${disk.mount}:${index}`}>
<span><strong>{disk.mount ?? disk.device ?? "Диск"}</strong><small>{[disk.device, disk.filesystem].filter(Boolean).join(" · ")}</small></span>
<span><strong>{formatPercent(disk.usedPercent)}</strong><small>{formatUsedTotal(disk.usedBytes, disk.totalBytes)}</small></span>
</div>
))}
{!current?.disks.length ? <div className="device-manager-panel-empty">Данные о дисках ещё не поступили.</div> : null}
</div>
</section>
<section className="host-monitoring-series-grid" aria-label="Аппаратная телеметрия VPS">
<HostTelemetrySeries label="CPU" value={formatPercent(current?.cpu.usagePercent)} resource={formatLoad(current?.cpu)} values={cpuHistory} domain={cpuDomain} />
<HostTelemetrySeries label="RAM" value={formatPercent(current?.memory.usedPercent)} resource={formatUsedTotal(current?.memory.usedBytes, current?.memory.totalBytes)} values={memoryHistory} domain={memoryDomain} />
<HostTelemetrySeries label="NETWORK RX" value={formatRate(networkRate.received)} resource={networkRate.received == null ? "нет данных" : "входящий трафик"} values={receiveHistory} />
<HostTelemetrySeries label="NETWORK TX" value={formatRate(networkRate.sent)} resource={networkRate.sent == null ? "нет данных" : "исходящий трафик"} values={sendHistory} />
</section>
<section className="host-telemetry-section">
<div className="host-telemetry-section__heading"><div><small>NETWORK</small><h3>Сетевые интерфейсы</h3></div><StatusBadge>{current?.network.length ?? 0}</StatusBadge></div>
<div className="host-telemetry-list">
{(current?.network ?? []).filter((item) => item.interface !== "lo").map((item, index) => (
<div className="host-telemetry-list__row" key={`${item.interface}:${index}`}>
<span><strong>{item.interface ?? "Интерфейс"}</strong><small>{formatPackets(item.packetsReceived, item.packetsSent)}</small></span>
<span><strong>↓ {formatMetricBytes(item.bytesReceived)}</strong><small>↑ {formatMetricBytes(item.bytesSent)}</small></span>
</div>
))}
{!current?.network.filter((item) => item.interface !== "lo").length ? <div className="device-manager-panel-empty">Сетевые счётчики ещё не поступили.</div> : null}
</div>
</section>
</div>
<section className="host-telemetry-section">
<div className="host-telemetry-section__heading">
<div><small>PROCESSING RUNTIME</small><h3>Сервисы VPS</h3><p>Состояние systemd-юнитов и их ресурсный профиль.</p></div>
<StatusBadge tone={runtimeServices.some((service) => service.activeState === "failed") ? "danger" : "success"}>{runtimeServices.length} units</StatusBadge>
<GlassSurface className="host-monitoring-hardware" padding="lg">
<InfrastructureSectionHeading
eyebrow="HARDWARE"
title={current?.hardware.hostname ?? host.hostKey}
description={[current?.hardware.platform, current?.hardware.architecture].filter(Boolean).join(" / ") || "Аппаратный профиль недоступен"}
status={telemetry.state}
statusTone={freshnessTone(telemetry.freshness)}
/>
<div className="host-monitoring-hardware-facts">
<dl>
<TelemetryFact label="Процессор" value={current?.hardware.cpuModel} />
<TelemetryFact label="Логические ядра" value={formatNullable(current?.hardware.logicalProcessors)} />
<TelemetryFact label="Память занята" value={formatUsedTotal(current?.memory.usedBytes, current?.memory.totalBytes)} />
<TelemetryFact label="Uptime" value={formatDuration(current?.system.uptimeSeconds)} />
</dl>
<dl>
<TelemetryFact label="Платформа" value={[current?.hardware.platform, current?.hardware.architecture].filter(Boolean).join(" / ") || null} />
<TelemetryFact label="Kernel" value={current?.hardware.kernelRelease} />
<TelemetryFact label="Процессы" value={formatNullable(current?.system.processes.total)} />
<TelemetryFact label="Load average" value={formatLoad(current?.cpu)} />
</dl>
</div>
<div className="host-telemetry-service-grid">
{runtimeServices.map((service, index) => (
<div className="host-telemetry-service" key={`${service.name}:${index}`}>
<span><strong>{service.name ?? "systemd unit"}</strong><small>{service.subState ?? service.loadState ?? "—"}</small></span>
<span><StatusBadge tone={service.activeState === "active" ? "success" : service.activeState === "failed" ? "danger" : "warning"}>{service.activeState ?? "unknown"}</StatusBadge><small>{formatMetricBytes(service.memoryBytes)}</small></span>
<div className="host-monitoring-disk-list">
{(current?.disks ?? []).map((disk, index) => (
<div key={`${disk.device}:${disk.mount}:${index}`}>
<span>Диск {disk.mount ?? disk.device ?? "—"}</span>
<strong>{formatUsedTotal(disk.usedBytes, disk.totalBytes)}</strong>
</div>
))}
{!runtimeServices.length ? <div className="device-manager-panel-empty">Состояние сервисов ещё не поступило.</div> : null}
{!current?.disks.length ? <div className="infrastructure-empty">Данные о дисках ещё не поступили.</div> : null}
</div>
</GlassSurface>
<section className="infrastructure-section">
<InfrastructureSectionHeading
eyebrow="NETWORK"
title="Сетевые интерфейсы"
description="Счётчики трафика и ошибок по активным интерфейсам VPS."
status={russianCount(networkInterfaces.length, "интерфейс", "интерфейса", "интерфейсов")}
/>
{networkInterfaces.length ? (
<div className="host-monitoring-network-grid">
{networkInterfaces.map((item, index) => (
<GlassSurface className="host-monitoring-network-card" padding="md" tone="soft" key={`${item.interface}:${index}`}>
<header><strong>{item.interface ?? "Интерфейс"}</strong><small>{formatPackets(item.packetsReceived, item.packetsSent)}</small></header>
<dl>
<div><dt>Получено</dt><dd>{formatMetricBytes(item.bytesReceived)}</dd></div>
<div><dt>Отправлено</dt><dd>{formatMetricBytes(item.bytesSent)}</dd></div>
<div><dt>Ошибки RX / TX</dt><dd>{formatNullable(item.errorsReceived)} / {formatNullable(item.errorsSent)}</dd></div>
<div><dt>Потери RX / TX</dt><dd>{formatNullable(item.droppedReceived)} / {formatNullable(item.droppedSent)}</dd></div>
</dl>
</GlassSurface>
))}
</div>
) : <div className="infrastructure-empty">Сетевые счётчики ещё не поступили.</div>}
</section>
<section className="host-telemetry-section host-telemetry-evidence">
<div><small>ONTOLOGY / OBSERVATION</small><strong>{telemetry.observation?.entityId ?? "observation.observation"}</strong></div>
<div><small>Источник</small><strong>{current ? `${current.source.agent} ${current.source.agentVersion}` : "—"}</strong></div>
<div><small>Последнее наблюдение</small><strong>{formatDate(telemetry.observedAt)}</strong></div>
<div><small>Связанные сервисы</small><strong>{services.length}</strong></div>
<section className="infrastructure-section">
<InfrastructureSectionHeading
eyebrow="PROCESSING RUNTIME"
title="Сервисы VPS"
description={`Состояние systemd-юнитов; с хостом связано ${russianCount(services.length, "сервис", "сервиса", "сервисов")} Device Core.`}
status={russianCount(runtimeServices.length, "юнит", "юнита", "юнитов")}
statusTone={runtimeServices.some((service) => service.activeState === "failed") ? "danger" : runtimeServices.length ? "success" : "warning"}
/>
{runtimeServices.length ? (
<div className="infrastructure-runtime-grid">
{runtimeServices.map((service, index) => (
<GlassSurface className="host-monitoring-runtime-card" padding="md" tone="soft" key={`${service.name}:${index}`}>
<header>
<div><span className="infrastructure-eyebrow">SYSTEMD UNIT</span><h3>{service.name ?? "systemd unit"}</h3><code>{service.subState ?? "—"}</code></div>
<StatusBadge tone={service.activeState === "active" ? "success" : service.activeState === "failed" ? "danger" : "warning"}>{service.activeState ?? "unknown"}</StatusBadge>
</header>
<dl>
<div><dt>Load</dt><dd>{service.loadState ?? "—"}</dd></div>
<div><dt>Память</dt><dd>{formatMetricBytes(service.memoryBytes)}</dd></div>
<div><dt>Перезапуски</dt><dd>{formatNullable(service.restarts)}</dd></div>
<div><dt>PID</dt><dd>{formatNullable(service.pid)}</dd></div>
</dl>
</GlassSurface>
))}
</div>
) : <div className="infrastructure-empty">Состояние сервисов ещё не поступило.</div>}
</section>
</div>
);
}
function TelemetryMetricCard({ label, value, points, detail }: { label: string; value: string; points: Array<number | null>; detail: string }) {
function InfrastructureCount({ label, value, detail }: { label: string; value: number; detail: string }) {
return (
<div className="host-telemetry-metric">
<span>{label}</span>
<strong>{value}</strong>
<Sparkline values={points} />
<small>{detail}</small>
<div className="infrastructure-count-card">
<span>{label}</span><strong>{value}</strong><small>{detail}</small>
</div>
);
}
function Sparkline({ values }: { values: Array<number | null> }) {
const normalized = values.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
if (normalized.length < 2) return <div className="host-telemetry-sparkline host-telemetry-sparkline--empty" />;
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 (
<header className="infrastructure-section-heading">
<div><span className="infrastructure-eyebrow">{eyebrow}</span><h3>{title}</h3><p>{description}</p></div>
<div className="infrastructure-section-actions"><StatusBadge tone={tone}>{status}</StatusBadge>{actions}</div>
</header>
);
}
function InfrastructureRegistryRow({ label, title, description, status }: { label: string; title: string; description: string; status: string }) {
return (
<div className="infrastructure-registry-row">
<div><span className="infrastructure-eyebrow">{label}</span><strong>{title}</strong><small>{description}</small></div>
<StatusBadge tone={statusTone(status)}>{status}</StatusBadge>
</div>
);
}
type TelemetryLineDomain = {
minimum: number;
maximum: number;
label?: string;
};
function HostTelemetrySeries({ label, values, value, resource, domain }: { label: string; values: Array<number | null>; value: string; resource?: string | null; domain?: TelemetryLineDomain | null }) {
const points = telemetryLinePoints(values, domain);
return (
<div className="host-monitoring-series">
<div>
<span className="host-monitoring-series__label"><span>{label}</span>{resource ? <small>{resource}</small> : null}</span>
<strong>{value}</strong>
</div>
<svg viewBox="0 0 100 38" preserveAspectRatio="none" role="img" aria-label={`${label}: ${value}`}>
<path d="M0 37 H100" />
{points ? <polyline points={points} /> : null}
</svg>
{domain?.label ? <small className="host-monitoring-series__range">{domain.label}</small> : null}
</div>
);
}
function telemetryLinePoints(values: Array<number | null>, 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 <svg className="host-telemetry-sparkline" viewBox="0 0 100 32" preserveAspectRatio="none" aria-hidden="true"><polyline points={points} /></svg>;
}
function percentageTelemetryWindow(values: Array<number | null>, 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 <div><small>{label}</small><strong>{value || "—"}</strong></div>;
return <div><dt>{label}</dt><dd>{value || "—"}</dd></div>;
}
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<HTMLElement>(".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) {
+628 -179
View File
@@ -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,
@@ -0,0 +1,36 @@
{
"schemaVersion": "nodedc.device-plane.device-control-core-migration-replay-checkpoint-recovery.v2",
"mode": "terminal-044-replay-checkpoint-forward-repair",
"failedIncidentAudit": "device-control-core-incident-audit-20260822-043",
"failedRecovery": {
"patchId": "device-control-core-migration-replay-recovery-20260822-044",
"artifactSha256": "b893d8c90f98943797d32f486d6477d58a3be69eb1291e28c4a4bbd2e96774b7",
"failure": "preflight-replay-checkpoint-race",
"startedApply": false
},
"sourcePredecessor": {
"path": "services/device-control-core/migrations/014_device_registry_profile_commands.sql",
"sha256": "751accf346b34d2774cc7b9572640d2c25fdb0b1db793ac32183b56f48e26508"
},
"sourceTarget": {
"path": "services/device-control-core/migrations/014_device_registry_profile_commands.sql",
"sha256": "38bd86b42828d44c7101d5433ddc36018e92eedeee37b9de296432ad676edd46"
},
"rootCause": "restarting-core-cycles-exact-committed-migration-checkpoints",
"repair": "migration-014-add-constraint-not-valid",
"databasePreflight": "exact-replay-checkpoint-005-007-009-011-and-final-compatible-rows",
"databaseRowMutation": "none",
"databaseSchemaOutcome": "exact-final-migration-016-validated-command-kind-check",
"runtimeAction": "build+recreate-device-control-core-only",
"runtimePredecessor": "proven-degraded-restarting-exact-preapply-image",
"preservedServices": [
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target"
],
"databaseVolume": "nodedc-device-plane-postgres-data",
"publicIngress": "disabled",
"edgeChannel": "core-initiated-pinned-mtls-registered-edges-only",
"rollback": "source+exact-degraded-predecessor-image-runtime"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -0,0 +1,106 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const sourceRoot = resolve(scriptDir, "../..");
const artifactDir = resolve(
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|| resolve(scriptDir, "../deploy-artifacts"),
);
const [
patchId = "device-control-core-migration-replay-checkpoint-recovery-20260822-046",
...extra
] = process.argv.slice(2);
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
throw new Error(
"usage: build-device-control-core-migration-replay-checkpoint-recovery-artifact.mjs [patch-id]",
);
}
const entries = [
"services/device-control-core/migrations/014_device_registry_profile_commands.sql",
"deployment/device-control-core-migration-replay-checkpoint-recovery-v2.json",
];
const stage = await mkdtemp(join(tmpdir(), "nodedc-control-core-replay-checkpoint-"));
const payload = join(stage, "payload");
const target = resolve(artifactDir, `nodedc-device-plane-${patchId}.tgz`);
try {
const descriptor = JSON.parse(await readFile(resolve(sourceRoot, entries[1]), "utf8"));
const targetBytes = await readFile(resolve(sourceRoot, entries[0]));
const targetSha256 = createHash("sha256").update(targetBytes).digest("hex");
if (
descriptor.schemaVersion
!== "nodedc.device-plane.device-control-core-migration-replay-checkpoint-recovery.v2"
|| descriptor.mode !== "terminal-044-replay-checkpoint-forward-repair"
|| descriptor.failedRecovery?.patchId
!== "device-control-core-migration-replay-recovery-20260822-044"
|| descriptor.failedRecovery?.artifactSha256
!== "b893d8c90f98943797d32f486d6477d58a3be69eb1291e28c4a4bbd2e96774b7"
|| descriptor.failedRecovery?.startedApply !== false
|| descriptor.sourceTarget.path !== entries[0]
|| descriptor.sourceTarget.sha256 !== targetSha256
|| descriptor.databaseRowMutation !== "none"
|| descriptor.runtimeAction !== "build+recreate-device-control-core-only"
) {
throw new Error(
"device_control_core_migration_replay_checkpoint_recovery_descriptor_mismatch",
);
}
for (const entry of entries) {
await mkdir(dirname(join(payload, entry)), { recursive: true });
await cp(resolve(sourceRoot, entry), join(payload, entry), { force: true });
}
await writeFile(
join(stage, "manifest.env"),
`id=${patchId}\ncomponent=device-plane\ntype=app-overlay\n`,
"utf8",
);
await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, "utf8");
await mkdir(artifactDir, { recursive: true });
const tar = spawnSync(
"python3",
["-c", canonicalTarScript(), target, stage],
{ encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
);
if (tar.status !== 0) throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
const sha256 = createHash("sha256")
.update(await readFile(target))
.digest("hex");
console.log(JSON.stringify({
ok: true,
patchId,
component: "device-plane",
artifact: target,
sha256,
entries,
build: ["nodedc/device-control-core:local"],
services: ["device-control-core"],
transition: descriptor.mode,
databaseRowMutation: descriptor.databaseRowMutation,
runtimeAction: descriptor.runtimeAction,
}, null, 2));
} finally {
await rm(stage, { recursive: true, force: true });
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
"root=pathlib.Path(sys.argv[2])",
"with open(sys.argv[1],'wb') as out:",
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
" for top in ('manifest.env','files.txt','payload'):",
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
" for x in paths:",
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix()); info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
].join("\n");
}
@@ -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");
}
@@ -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"
+10 -3
View File
@@ -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)
@@ -94,7 +94,16 @@ class DeviceControlCoreMigrationReplayAuditArtifactTest(unittest.TestCase):
def test_database_audit_returns_all_mismatches_without_mutation(self):
database_result = mock.Mock(
returncode=0,
stdout="2\t0\tfalse\tfalse\tfalse\n",
stdout=(
"2\t0\tfalse\tfalse\tfalse\t"
+ json.dumps(
sorted(
RUNNER.DEVICE_PLANE_CONTROL_CORE_REPLAY_011_COMMAND_KINDS
),
separators=(",", ":"),
)
+ "\n"
),
stderr="",
)
with mock.patch.object(
@@ -117,7 +126,12 @@ class DeviceControlCoreMigrationReplayAuditArtifactTest(unittest.TestCase):
self.assertFalse(evidence["constraintValidated"])
self.assertFalse(evidence["constraintCoversFinalKinds"])
self.assertFalse(evidence["hostTelemetryTableAbsent"])
self.assertTrue(evidence["constraintMatchesReplay011Kinds"])
self.assertEqual(evidence["constraintPhase"], "replay-011")
self.assertTrue(evidence["constraintMatchesKnownReplayCheckpoint"])
self.assertFalse(evidence["recovery044Ready"])
self.assertFalse(evidence["checkpointRecoveryReady"])
self.assertFalse(evidence["finalStateReady"])
command = run.call_args.args[0]
query = command[command.index("-c") + 1]
self.assertTrue(query.lstrip().lower().startswith("with constraint_state"))
@@ -129,10 +143,17 @@ class DeviceControlCoreMigrationReplayAuditArtifactTest(unittest.TestCase):
)
)
def test_recovery_rejection_prints_five_bounded_values(self):
def test_recovery_rejection_prints_bounded_database_evidence(self):
database_result = mock.Mock(
returncode=0,
stdout="0\t0\ttrue\ttrue\ttrue\n",
stdout=(
"0\t0\ttrue\ttrue\ttrue\t"
+ json.dumps(
sorted(RUNNER.DEVICE_PLANE_CONTROL_CORE_FINAL_COMMAND_KINDS),
separators=(",", ":"),
)
+ "\n"
),
stderr="",
)
output = io.StringIO()
@@ -156,9 +177,135 @@ class DeviceControlCoreMigrationReplayAuditArtifactTest(unittest.TestCase):
"constraint_validated=true",
"constraint_covers_final_kinds=true",
"host_telemetry_table_absent=true",
"constraint_matches_replay_011_kinds=false",
"constraint_phase=final-016",
"constraint_matches_final_016_kinds=true",
"recovery_final_state_ready=false",
):
self.assertIn(marker, rendered)
def test_audit_preflight_resolves_recovery_from_canonical_inbox(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-control-core-migration-replay-preflight-",
) as directory:
root = Path(directory)
inbox = root / "inbox"
runner_tmp = root / "runner-tmp"
source = root / "device-plane"
inbox.mkdir()
runner_tmp.mkdir()
live_migration = (
source / RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
)
live_migration.parent.mkdir(parents=True)
live_migration.write_bytes(b"migration-014-predecessor")
recovery_artifact = (
inbox
/ RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT
)
recovery_artifact.write_bytes(b"reviewed-recovery-044")
core = {
"containerId": "c" * 64,
"imageId": RUNNER.DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID,
"status": "running",
"health": "unhealthy",
"restartCount": 3,
}
database = {
"invalidCommandKindCount": 0,
"triggeringReceiptCount": 0,
"constraintValidated": True,
"constraintCoversFinalKinds": True,
"hostTelemetryTableAbsent": True,
"constraintMatchesReplay011Kinds": True,
"constraintPhase": "replay-011",
"constraintMatchesKnownReplayCheckpoint": True,
"constraintMatchesFinalKinds": False,
"recovery044Ready": False,
"checkpointRecoveryReady": False,
"finalStateReady": False,
}
failure_evidence = {
"firstBackup": root / "first-backup",
"secondBackup": root / "second-backup",
}
recovery_manifest = {
"id": RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID,
"component": "device-plane",
"type": "app-overlay",
}
with (
mock.patch.object(RUNNER, "INBOX", inbox),
mock.patch.object(RUNNER, "TMP_DIR", runner_tmp),
mock.patch.object(
RUNNER,
"DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256",
hashlib.sha256(recovery_artifact.read_bytes()).hexdigest(),
),
mock.patch.object(
RUNNER,
"DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256",
hashlib.sha256(live_migration.read_bytes()).hexdigest(),
),
mock.patch.object(
RUNNER,
"validate_device_plane_control_core_migration_replay_audit_payload",
return_value=(
RUNNER.expected_device_plane_control_core_migration_replay_audit_descriptor()
),
),
mock.patch.object(
RUNNER,
"validate_device_plane_control_core_double_failure_evidence",
return_value=failure_evidence,
),
mock.patch.object(
RUNNER,
"load_artifact",
return_value=(
recovery_manifest,
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ENTRIES,
root / "recovery-payload",
),
),
mock.patch.object(
RUNNER,
"validate_device_plane_control_core_migration_replay_recovery_payload",
),
mock.patch.object(
RUNNER,
"load_state",
return_value=[],
) as load_state,
mock.patch.object(RUNNER, "component_root", return_value=source),
mock.patch.object(
RUNNER,
"validate_device_plane_control_core_migration_replay_preserved_runtime",
return_value={"current": {"services": []}, "core": core},
),
mock.patch.object(
RUNNER,
"collect_device_plane_control_core_migration_replay_database_evidence",
return_value=database,
) as collect_database,
):
evidence = (
RUNNER.validate_device_plane_control_core_migration_replay_audit_evidence(
root / "audit-payload"
)
)
self.assertEqual(evidence["recoveryArtifact"], recovery_artifact)
self.assertEqual(evidence["database"], database)
self.assertEqual(
[call.args for call in load_state.call_args_list],
[(RUNNER.STATE_FILE,), (RUNNER.FAILED_STATE_FILE,)],
)
collect_database.assert_called_once_with(
enforce_recovery_invariants=False,
)
def test_plan_and_apply_paths_preserve_plan_only_boundary(self):
plan_source = inspect.getsource(RUNNER.plan_artifact)
apply_source = inspect.getsource(RUNNER.apply_artifact)
@@ -0,0 +1,442 @@
#!/usr/bin/env python3
import hashlib
import importlib.machinery
import importlib.util
import inspect
import json
import os
import re
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent
RUNNER_PATH = Path(
os.environ.get(
"NODEDC_DEPLOY_RUNNER",
SCRIPT_DIR.parents[2] / "platform/infra/deploy-runner/nodedc-deploy",
)
)
BUILDER = (
SCRIPT_DIR
/ "build-device-control-core-migration-replay-checkpoint-recovery-artifact.mjs"
)
LEGACY_BUILDER = (
SCRIPT_DIR
/ "build-device-control-core-migration-replay-recovery-artifact.mjs"
)
MIGRATION_ROOT = SCRIPT_DIR.parents[1] / "services/device-control-core/migrations"
def load_runner():
loader = importlib.machinery.SourceFileLoader(
"nodedc_control_core_migration_replay_checkpoint_runner_under_test",
str(RUNNER_PATH),
)
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
RUNNER = load_runner()
def database_stdout(
kinds,
*,
invalid=0,
triggering=8,
validated="false",
covers_final="false",
telemetry_absent="true",
):
return "\t".join(
(
str(invalid),
str(triggering),
validated,
covers_final,
telemetry_absent,
json.dumps(sorted(kinds), separators=(",", ":")),
)
) + "\n"
class DeviceControlCoreMigrationReplayCheckpointRecoveryArtifactTest(
unittest.TestCase
):
def build(self, artifact_dir):
environment = os.environ.copy()
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
result = subprocess.run(
[
"node",
str(BUILDER),
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_PATCH_ID,
],
check=True,
capture_output=True,
text=True,
env=environment,
)
return json.loads(result.stdout)
def test_artifact_is_deterministic_exact_new_identity(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-control-core-checkpoint-recovery-",
) as directory:
artifact_dir = Path(directory)
first = self.build(artifact_dir)
first_bytes = Path(first["artifact"]).read_bytes()
second = self.build(artifact_dir)
second_bytes = Path(second["artifact"]).read_bytes()
self.assertEqual(first_bytes, second_bytes)
self.assertEqual(
first["sha256"],
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ARTIFACT_SHA256,
)
self.assertEqual(first["services"], ["device-control-core"])
self.assertEqual(first["databaseRowMutation"], "none")
self.assertEqual(
first["entries"],
list(
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ENTRIES
),
)
with tarfile.open(first["artifact"], "r:gz") as archive:
descriptor = json.loads(
archive.extractfile(
"payload/"
+ RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_REL
).read().decode("utf-8")
)
migration = archive.extractfile(
"payload/" + RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
).read()
self.assertEqual(
descriptor,
RUNNER.expected_device_plane_control_core_migration_replay_checkpoint_recovery_descriptor(),
)
self.assertEqual(
hashlib.sha256(migration).hexdigest(),
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_TARGET_SHA256,
)
self.assertIn(b")) not valid;", migration.lower())
self.assertIsNone(
re.search(
rb"(?im)^\s*(?:delete|update|insert|truncate)\b",
migration,
)
)
def test_runner_selects_only_core_and_exact_build(self):
entries = (
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_CHECKPOINT_RECOVERY_ENTRIES
)
self.assertEqual(
RUNNER.component_services("device-plane", entries),
("device-control-core",),
)
builds = RUNNER.component_builds("device-plane", entries)
self.assertEqual(
builds,
((
RUNNER.DEVICE_PLANE_ROOT,
(
"build",
"--no-cache",
"--network=host",
"-f",
"services/device-control-core/Dockerfile",
"-t",
RUNNER.DEVICE_PLANE_CONTROL_CORE_IMAGE,
".",
),
),),
)
def test_checkpoint_sets_match_every_committed_replay_migration(self):
for phase, expected in RUNNER.DEVICE_PLANE_CONTROL_CORE_REPLAY_CHECKPOINTS:
migration_number = phase.split("-", 1)[1]
migration = next(MIGRATION_ROOT.glob(f"{migration_number}_*.sql"))
command_kinds = tuple(re.findall(r"'([^']+)'", migration.read_text()))
with self.subTest(phase=phase):
self.assertEqual(command_kinds, expected)
self.assertIn("not valid", migration.read_text().lower())
def test_every_exact_replay_checkpoint_is_accepted(self):
for phase, kinds in RUNNER.DEVICE_PLANE_CONTROL_CORE_REPLAY_CHECKPOINTS:
database_result = mock.Mock(
returncode=0,
stdout=database_stdout(kinds),
stderr="",
)
with self.subTest(phase=phase), mock.patch.object(
RUNNER,
"device_plane_service_container_ids",
return_value=("p" * 64,),
), mock.patch.object(
RUNNER.subprocess,
"run",
return_value=database_result,
):
evidence = (
RUNNER.collect_device_plane_control_core_migration_replay_database_evidence(
expected_state="replay-checkpoint-predecessor",
)
)
self.assertEqual(evidence["constraintPhase"], phase)
self.assertTrue(
evidence["constraintMatchesKnownReplayCheckpoint"]
)
self.assertTrue(evidence["checkpointRecoveryReady"])
self.assertFalse(evidence["finalStateReady"])
def test_unknown_missing_and_validated_checkpoint_states_are_rejected(self):
cases = (
(
"unknown",
database_stdout(("owner_scope.ensure", "unknown.ensure")),
),
(
"missing",
database_stdout((), validated="missing"),
),
(
"validated-checkpoint",
database_stdout(
RUNNER.DEVICE_PLANE_CONTROL_CORE_REPLAY_005_COMMAND_KINDS,
validated="true",
),
),
)
for name, stdout in cases:
database_result = mock.Mock(
returncode=0,
stdout=stdout,
stderr="",
)
with self.subTest(name=name), mock.patch.object(
RUNNER,
"device_plane_service_container_ids",
return_value=("p" * 64,),
), mock.patch.object(
RUNNER.subprocess,
"run",
return_value=database_result,
), self.assertRaises(RUNNER.DeployError):
RUNNER.collect_device_plane_control_core_migration_replay_database_evidence(
expected_state="replay-checkpoint-predecessor",
)
def test_final_acceptance_requires_exact_migration_016_set(self):
database_result = mock.Mock(
returncode=0,
stdout=database_stdout(
RUNNER.DEVICE_PLANE_CONTROL_CORE_FINAL_COMMAND_KINDS,
validated="true",
covers_final="true",
),
stderr="",
)
with mock.patch.object(
RUNNER,
"device_plane_service_container_ids",
return_value=("p" * 64,),
), mock.patch.object(
RUNNER.subprocess,
"run",
return_value=database_result,
):
evidence = (
RUNNER.collect_device_plane_control_core_migration_replay_database_evidence(
expected_state="final",
)
)
self.assertEqual(evidence["constraintPhase"], "final-016")
self.assertTrue(evidence["constraintMatchesFinalKinds"])
self.assertTrue(evidence["finalStateReady"])
self.assertFalse(evidence["checkpointRecoveryReady"])
def test_terminal_044_failure_evidence_is_exact_and_preapply(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-control-core-recovery-044-failure-",
) as directory:
root = Path(directory)
artifact_dir = root / "artifact"
failed_dir = root / "failed"
runner_tmp = root / "tmp"
state_file = root / "applied.jsonl"
failed_state_file = root / "failed.jsonl"
artifact_dir.mkdir()
failed_dir.mkdir()
runner_tmp.mkdir()
environment = os.environ.copy()
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
subprocess.run(
[
"node",
str(LEGACY_BUILDER),
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID,
],
check=True,
capture_output=True,
text=True,
env=environment,
)
source = (
artifact_dir
/ RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT
)
failed_artifact = (
failed_dir
/ RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_FAILED_ARTIFACT
)
source.replace(failed_artifact)
state_file.write_text("", encoding="utf-8")
record = {
"artifact": failed_artifact.name,
"backup_id": None,
"component": "device-plane",
"failed_at": (
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_FAILED_AT
),
"id": RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_PATCH_ID,
"message": (
"Device Control Core migration recovery database invariant mismatch"
),
"rollback_status": "not-required",
"sha256": (
RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_REPLAY_RECOVERY_ARTIFACT_SHA256
),
"started_apply": False,
"status": "failed",
}
failed_state_file.write_text(
json.dumps(record) + "\n",
encoding="utf-8",
)
with mock.patch.object(RUNNER, "FAILED_DIR", failed_dir), mock.patch.object(
RUNNER,
"STATE_FILE",
state_file,
), mock.patch.object(
RUNNER,
"FAILED_STATE_FILE",
failed_state_file,
), mock.patch.object(RUNNER, "TMP_DIR", runner_tmp):
evidence = (
RUNNER.validate_device_plane_control_core_migration_replay_recovery_failure()
)
self.assertEqual(evidence["record"], record)
def test_checkpoint_recovery_preflight_reaches_database_collector(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-control-core-checkpoint-preflight-",
) as directory:
root = Path(directory)
migration = (
root / RUNNER.DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_REL
)
migration.parent.mkdir(parents=True)
migration.write_bytes(b"migration-014-predecessor")
core = {
"containerId": "c" * 64,
"imageId": RUNNER.DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID,
"status": "restarting",
"health": "unhealthy",
"restartCount": 280,
}
runtime = {"core": core, "current": {"services": [core]}}
database = {
"constraintPhase": "replay-007",
"checkpointRecoveryReady": True,
}
descriptor = (
RUNNER.expected_device_plane_control_core_migration_replay_checkpoint_recovery_descriptor()
)
with mock.patch.object(
RUNNER,
"validate_device_plane_control_core_migration_replay_checkpoint_recovery_payload",
return_value=descriptor,
), mock.patch.object(
RUNNER,
"validate_device_plane_control_core_double_failure_evidence",
return_value={
"firstBackup": root / "first",
"secondBackup": root / "second",
},
), mock.patch.object(
RUNNER,
"validate_device_plane_control_core_migration_replay_recovery_failure",
return_value={"record": {"started_apply": False}},
), mock.patch.object(
RUNNER,
"component_root",
return_value=root,
), mock.patch.object(
RUNNER,
"DEVICE_PLANE_CONTROL_CORE_MIGRATION_014_PREDECESSOR_SHA256",
hashlib.sha256(migration.read_bytes()).hexdigest(),
), mock.patch.object(
RUNNER,
"validate_device_plane_control_core_migration_replay_preserved_runtime",
return_value=runtime,
), mock.patch.object(
RUNNER,
"inspect_optional_local_image",
return_value=RUNNER.DEVICE_PLANE_CONTROL_CORE_V3_PREAPPLY_IMAGE_ID,
), mock.patch.object(
RUNNER,
"collect_device_plane_control_core_migration_replay_database_evidence",
return_value=database,
) as collector:
evidence = (
RUNNER.validate_device_plane_control_core_migration_replay_checkpoint_recovery_evidence(
root
)
)
self.assertEqual(evidence["database"], database)
collector.assert_called_once_with(
expected_state="replay-checkpoint-predecessor",
)
def test_v2_rollback_and_acceptance_are_registered(self):
rollback_source = inspect.getsource(RUNNER.rollback_device_plane_apply)
health_source = inspect.getsource(RUNNER.run_healthchecks)
preflight_source = inspect.getsource(
RUNNER.validate_device_plane_control_core_migration_replay_checkpoint_recovery_evidence
)
acceptance_source = inspect.getsource(
RUNNER.accept_device_plane_control_core_migration_replay_checkpoint_recovery
)
self.assertIn(
"is_device_plane_control_core_migration_replay_checkpoint_recovery_slice",
rollback_source,
)
self.assertIn(
"accept_device_plane_control_core_migration_replay_checkpoint_recovery",
health_source,
)
self.assertIn(
'expected_state="replay-checkpoint-predecessor"',
preflight_source,
)
self.assertIn(
"validate_device_plane_control_core_migration_replay_recovery_failure",
preflight_source,
)
self.assertIn('expected_state="final"', acceptance_source)
if __name__ == "__main__":
unittest.main()
@@ -25,6 +25,10 @@ BUILDER = (
SCRIPT_DIR
/ "build-device-control-core-migration-replay-recovery-artifact.mjs"
)
MIGRATION_011 = (
SCRIPT_DIR.parents[1]
/ "services/device-control-core/migrations/011_device_control_resource_commands.sql"
)
def load_runner():
@@ -135,7 +139,16 @@ class DeviceControlCoreMigrationReplayRecoveryArtifactTest(unittest.TestCase):
def test_database_preflight_is_read_only_and_covers_live_receipts(self):
database_result = mock.Mock(
returncode=0,
stdout="0\t3\ttrue\ttrue\ttrue\n",
stdout=(
"0\t8\tfalse\tfalse\ttrue\t"
+ json.dumps(
sorted(
RUNNER.DEVICE_PLANE_CONTROL_CORE_REPLAY_011_COMMAND_KINDS
),
separators=(",", ":"),
)
+ "\n"
),
stderr="",
)
with mock.patch.object(
@@ -148,11 +161,20 @@ class DeviceControlCoreMigrationReplayRecoveryArtifactTest(unittest.TestCase):
return_value=database_result,
) as run:
evidence = (
RUNNER.collect_device_plane_control_core_migration_replay_database_evidence()
RUNNER.collect_device_plane_control_core_migration_replay_database_evidence(
expected_state="replay-011-predecessor",
)
)
self.assertEqual(evidence["invalidCommandKindCount"], 0)
self.assertEqual(evidence["triggeringReceiptCount"], 3)
self.assertEqual(evidence["triggeringReceiptCount"], 8)
self.assertFalse(evidence["constraintValidated"])
self.assertFalse(evidence["constraintCoversFinalKinds"])
self.assertTrue(evidence["constraintMatchesReplay011Kinds"])
self.assertEqual(evidence["constraintPhase"], "replay-011")
self.assertTrue(evidence["recovery044Ready"])
self.assertTrue(evidence["checkpointRecoveryReady"])
self.assertFalse(evidence["finalStateReady"])
command = run.call_args.args[0]
query = command[command.index("-c") + 1]
self.assertTrue(query.lstrip().lower().startswith("with constraint_state"))
@@ -163,12 +185,89 @@ class DeviceControlCoreMigrationReplayRecoveryArtifactTest(unittest.TestCase):
query,
)
)
self.assertIn("regexp_matches", query)
self.assertIn("array_to_json", query)
def test_runner_predecessor_kind_set_matches_migration_011(self):
migration = MIGRATION_011.read_text(encoding="utf-8")
command_kinds = tuple(re.findall(r"'([^']+)'", migration))
self.assertEqual(
command_kinds,
RUNNER.DEVICE_PLANE_CONTROL_CORE_REPLAY_011_COMMAND_KINDS,
)
def test_database_acceptance_requires_final_validated_constraint(self):
database_result = mock.Mock(
returncode=0,
stdout=(
"0\t8\ttrue\ttrue\ttrue\t"
+ json.dumps(
sorted(RUNNER.DEVICE_PLANE_CONTROL_CORE_FINAL_COMMAND_KINDS),
separators=(",", ":"),
)
+ "\n"
),
stderr="",
)
with mock.patch.object(
RUNNER,
"device_plane_service_container_ids",
return_value=("p" * 64,),
), mock.patch.object(
RUNNER.subprocess,
"run",
return_value=database_result,
):
evidence = (
RUNNER.collect_device_plane_control_core_migration_replay_database_evidence(
expected_state="final",
)
)
self.assertTrue(evidence["constraintValidated"])
self.assertTrue(evidence["constraintCoversFinalKinds"])
self.assertFalse(evidence["constraintMatchesReplay011Kinds"])
self.assertEqual(evidence["constraintPhase"], "final-016")
self.assertTrue(evidence["constraintMatchesFinalKinds"])
self.assertFalse(evidence["recovery044Ready"])
self.assertFalse(evidence["checkpointRecoveryReady"])
self.assertTrue(evidence["finalStateReady"])
def test_database_preflight_rejects_ambiguous_constraint_shape(self):
database_result = mock.Mock(
returncode=0,
stdout=(
"0\t8\tfalse\tfalse\ttrue\t"
+ json.dumps(["owner_scope.ensure", "unknown.ensure"])
+ "\n"
),
stderr="",
)
with mock.patch.object(
RUNNER,
"device_plane_service_container_ids",
return_value=("p" * 64,),
), mock.patch.object(
RUNNER.subprocess,
"run",
return_value=database_result,
), self.assertRaises(RUNNER.DeployError):
RUNNER.collect_device_plane_control_core_migration_replay_database_evidence(
expected_state="replay-011-predecessor",
)
def test_rollback_accepts_exact_degraded_predecessor_boundary(self):
rollback_source = inspect.getsource(RUNNER.rollback_device_plane_apply)
acceptance_source = inspect.getsource(
RUNNER.accept_device_plane_control_core_rollback_runtime
)
preflight_source = inspect.getsource(
RUNNER.validate_device_plane_control_core_migration_replay_recovery_evidence
)
recovery_acceptance_source = inspect.getsource(
RUNNER.accept_device_plane_control_core_migration_replay_recovery
)
self.assertIn(
"is_device_plane_control_core_migration_replay_recovery_slice",
rollback_source,
@@ -176,6 +275,14 @@ class DeviceControlCoreMigrationReplayRecoveryArtifactTest(unittest.TestCase):
self.assertIn("retag_device_plane_control_core_image", rollback_source)
self.assertIn("not predecessor_was_healthy", acceptance_source)
self.assertIn("changed preserved service", acceptance_source)
self.assertIn(
'expected_state="replay-011-predecessor"',
preflight_source,
)
self.assertIn(
'expected_state="final"',
recovery_acceptance_source,
)
if __name__ == "__main__":
@@ -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)
@@ -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")