diff --git a/apps/device-manager/index.html b/apps/device-manager/index.html
index 30163c1..bc870e3 100644
--- a/apps/device-manager/index.html
+++ b/apps/device-manager/index.html
@@ -4,6 +4,12 @@
+
+
+
+
+
+
NODE.DC Device Core
diff --git a/apps/device-manager/public/favicon.ico b/apps/device-manager/public/favicon.ico
new file mode 100644
index 0000000..973f3b2
Binary files /dev/null and b/apps/device-manager/public/favicon.ico differ
diff --git a/apps/device-manager/public/favicon/apple-touch-icon.png b/apps/device-manager/public/favicon/apple-touch-icon.png
new file mode 100644
index 0000000..5078c01
Binary files /dev/null and b/apps/device-manager/public/favicon/apple-touch-icon.png differ
diff --git a/apps/device-manager/public/favicon/favicon.ico b/apps/device-manager/public/favicon/favicon.ico
new file mode 100644
index 0000000..973f3b2
Binary files /dev/null and b/apps/device-manager/public/favicon/favicon.ico differ
diff --git a/apps/device-manager/public/favicon/icon-192.png b/apps/device-manager/public/favicon/icon-192.png
new file mode 100644
index 0000000..d553a58
Binary files /dev/null and b/apps/device-manager/public/favicon/icon-192.png differ
diff --git a/apps/device-manager/public/favicon/icon-512.png b/apps/device-manager/public/favicon/icon-512.png
new file mode 100644
index 0000000..a6f47b9
Binary files /dev/null and b/apps/device-manager/public/favicon/icon-512.png differ
diff --git a/apps/device-manager/public/favicon/icon-adaptive.svg b/apps/device-manager/public/favicon/icon-adaptive.svg
new file mode 100644
index 0000000..fee636f
--- /dev/null
+++ b/apps/device-manager/public/favicon/icon-adaptive.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/apps/device-manager/public/favicon/manifest.webmanifest.json b/apps/device-manager/public/favicon/manifest.webmanifest.json
new file mode 100644
index 0000000..f7256e8
--- /dev/null
+++ b/apps/device-manager/public/favicon/manifest.webmanifest.json
@@ -0,0 +1,29 @@
+{
+ "theme_color": "#eeeff4",
+ "background_color": "#eeeff4",
+ "display": "browser",
+ "scope": "/",
+ "start_url": "/",
+ "icons": [
+ {
+ "src": "icon-adaptive.svg",
+ "type": "image/svg+xml",
+ "sizes": "any"
+ },
+ {
+ "src": "icon-192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "icon-512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ },
+ {
+ "src": "apple-touch-icon.png",
+ "type": "image/png",
+ "sizes": "180x180"
+ }
+ ]
+}
diff --git a/deployment/device-manager-release-v6.json b/deployment/device-manager-release-v6.json
new file mode 100644
index 0000000..6f6ba06
--- /dev/null
+++ b/deployment/device-manager-release-v6.json
@@ -0,0 +1,41 @@
+{
+ "schemaVersion": "nodedc.device-plane.device-manager-release.v6",
+ "releaseId": "__PATCH_ID__",
+ "action": "upgrade",
+ "predecessor": {
+ "kind": "release",
+ "patchId": "device-manager-release-v5-20260822-034",
+ "artifactSha256": "acc1d2ae2cda66861054826928c25d01a2428e688cc8132a9c381831bf29ab5a"
+ },
+ "controlCorePredecessor": {
+ "patchId": "device-control-core-release-v2-20260821-030",
+ "artifactSha256": "8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454"
+ },
+ "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",
+ "gelios": "untouched-legacy-only",
+ "rollback": "restore-preapply-snapshot-preserve-manager-data"
+}
diff --git a/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs b/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs
index 19577f4..5b00307 100644
--- a/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs
+++ b/infra/deploy-runner/build-device-manager-control-plane-artifact.mjs
@@ -11,21 +11,24 @@ 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-v5-20260822-034", ...extra] = process.argv.slice(2);
+const [patchId = "device-manager-release-v6-20260822-035", ...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-v5-")
- ? "deployment/device-manager-release-v5.json"
- : patchId.startsWith("device-manager-release-v4-")
- ? "deployment/device-manager-release-v4.json"
- : patchId.startsWith("device-manager-release-v3-")
- ? "deployment/device-manager-release-v3.json"
- : "deployment/device-manager-release-v1.json";
+const descriptorPath = patchId.startsWith("device-manager-release-v6-")
+ ? "deployment/device-manager-release-v6.json"
+ : patchId.startsWith("device-manager-release-v5-")
+ ? "deployment/device-manager-release-v5.json"
+ : patchId.startsWith("device-manager-release-v4-")
+ ? "deployment/device-manager-release-v4.json"
+ : patchId.startsWith("device-manager-release-v3-")
+ ? "deployment/device-manager-release-v3.json"
+ : "deployment/device-manager-release-v1.json";
const isV3 = descriptorPath.endsWith("release-v3.json");
const isV4 = descriptorPath.endsWith("release-v4.json");
const isV5 = descriptorPath.endsWith("release-v5.json");
-const isPersistent = isV4 || isV5;
+const isV6 = descriptorPath.endsWith("release-v6.json");
+const isPersistent = isV4 || isV5 || isV6;
const isManagerOnly = isV3 || isPersistent;
const composeSource = resolve(devicePlaneRoot, "docker-compose.device-manager.yml");
const composeSourceSha256 = createHash("sha256").update(await readFile(composeSource)).digest("hex");
@@ -164,7 +167,56 @@ try {
: "restore-preapply-snapshot")
);
if (commonContractInvalid) throw new Error("device_manager_activation_successor_contract_mismatch");
- if (descriptorPath.endsWith("release-v5.json")) {
+ if (descriptorPath.endsWith("release-v6.json")) {
+ if (
+ descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v6"
+ || descriptor.predecessor?.kind !== "release"
+ || descriptor.predecessor?.patchId !== "device-manager-release-v5-20260822-034"
+ || descriptor.predecessor?.artifactSha256 !== "acc1d2ae2cda66861054826928c25d01a2428e688cc8132a9c381831bf29ab5a"
+ || descriptor.commandTransport !== "typed-service-ping-v1"
+ || 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.controlCorePredecessor?.patchId !== "device-control-core-release-v2-20260821-030"
+ || descriptor.controlCorePredecessor?.artifactSha256 !== "8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454"
+ || descriptor.edgeChannelPredecessor?.patchId !== "device-edge-core-channel-upgrade-v4-20260812-023"
+ || descriptor.edgeChannelPredecessor?.artifactSha256 !== "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
+ || descriptor.presentationPersistence !== "runner-managed-host-data-bind"
+ || descriptor.presentationDataHostPath !== "/volume1/docker/nodedc-device-plane/data/device-manager"
+ || descriptor.presentationDataContainerPath !== "/var/lib/nodedc-device-manager"
+ || descriptor.presentationDataOwnership !== "uid-1000-gid-1000-mode-0750"
+ || descriptor.presentationDataLifecycle !== "preserve-across-manager-recreate-and-source-rollback"
+ || descriptor.presentationPath !== "/var/lib/nodedc-device-manager/device-manager-presentation.json"
+ || descriptor.mediaRoot !== "/var/lib/nodedc-device-manager/media"
+ || descriptor.defaultAccentHex !== "#f5f5f5"
+ || descriptor.overviewLayout !== "mission-core-landing-stage-v1"
+ || descriptor.faviconSet !== "nodedc-adaptive-v1"
+ || descriptor.rollback !== "restore-preapply-snapshot-preserve-manager-data"
+ || descriptor.gelios !== "untouched-legacy-only"
+ ) throw new Error("device_manager_v6_favicon_contract_mismatch");
+ const faviconHashes = {
+ "favicon.ico": "f8933114a85646335ea5c94944f56d3cd8c48a6032016719f7905ff244ec0aa2",
+ "favicon/favicon.ico": "f8933114a85646335ea5c94944f56d3cd8c48a6032016719f7905ff244ec0aa2",
+ "favicon/icon-adaptive.svg": "481984e83997d786bb0a72ad1ee80037db13aef3a0792ab3109df95c2199b38e",
+ "favicon/apple-touch-icon.png": "afdccc28152a566e264e533ca218362f05d5bcec936c647f9a54f414c0bd4763",
+ "favicon/icon-192.png": "5b10a24feb4754f15c69761cef42f91a01f885d04095156b1a12e254875fdd4d",
+ "favicon/icon-512.png": "f98bac3dba59b7eefbe89f8bb8abc25567226a54ab7ed3b6b1a4caffbdd9ee15",
+ "favicon/manifest.webmanifest.json": "2a8ecdc6e6c64833f812ae02bbc0c7bd9b435e0cfa75cc21d6edf054d41275fc",
+ };
+ for (const [relativePath, expectedSha256] of Object.entries(faviconHashes)) {
+ const content = await readFile(join(payload, "services/device-manager/dist", relativePath));
+ const actualSha256 = createHash("sha256").update(content).digest("hex");
+ if (actualSha256 !== expectedSha256) throw new Error(`device_manager_v6_favicon_hash_mismatch:${relativePath}`);
+ }
+ const indexHtml = await readFile(join(payload, "services/device-manager/dist/index.html"), "utf8");
+ for (const requiredLink of [
+ 'href="/favicon/icon-adaptive.svg"',
+ 'href="/favicon/favicon.ico"',
+ 'href="/favicon/apple-touch-icon.png"',
+ 'href="/favicon/icon-192.png"',
+ 'href="/favicon/icon-512.png"',
+ 'href="/favicon/manifest.webmanifest.json"',
+ ]) if (!indexHtml.includes(requiredLink)) throw new Error(`device_manager_v6_favicon_link_missing:${requiredLink}`);
+ } else if (descriptorPath.endsWith("release-v5.json")) {
if (
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v5"
|| descriptor.predecessor?.kind !== "release"
diff --git a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py
index 240d8ed..5b0f0e9 100644
--- a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py
+++ b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py
@@ -458,10 +458,15 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
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)
+ any(
+ name.startswith("payload/services/device-control-core/")
+ for name in names
+ )
)
self.assertFalse(any(name.startswith("payload/packages/") for name in names))
- self.assertFalse(any(name.endswith((".test.mjs", ".map")) for name in names))
+ self.assertFalse(
+ any(name.endswith((".test.mjs", ".map")) for name in names)
+ )
template = json.loads(
(
DEVICE_CORE_ROOT
@@ -503,6 +508,68 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
)
)
+ def test_device_manager_release_v6_includes_canonical_favicon_set(self):
+ patch_id = "device-manager-release-v6-unit-001"
+ manifest, entries, names, result = self.assert_deterministic_artifact(
+ "build-device-manager-control-plane-artifact.mjs",
+ patch_id,
+ RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_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))
+ self.assertFalse(any(name.endswith((".test.mjs", ".map")) for name in names))
+ for name in (
+ "payload/services/device-manager/dist/favicon.ico",
+ "payload/services/device-manager/dist/favicon/favicon.ico",
+ "payload/services/device-manager/dist/favicon/icon-adaptive.svg",
+ "payload/services/device-manager/dist/favicon/apple-touch-icon.png",
+ "payload/services/device-manager/dist/favicon/icon-192.png",
+ "payload/services/device-manager/dist/favicon/icon-512.png",
+ "payload/services/device-manager/dist/favicon/manifest.webmanifest.json",
+ ):
+ self.assertIn(name, names)
+ template = json.loads(
+ (
+ DEVICE_CORE_ROOT
+ / "deployment/device-manager-release-v6.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.v6"
+ ),
+ boundaries=(
+ RUNNER.expected_device_plane_manager_release_v6_boundaries()
+ ),
+ expected_release_id=patch_id,
+ ),
+ descriptor,
+ )
+ self.assertEqual(descriptor["faviconSet"], "nodedc-adaptive-v1")
+ self.assertEqual(
+ descriptor["predecessor"],
+ {
+ "kind": "release",
+ "patchId": "device-manager-release-v5-20260822-034",
+ "artifactSha256": (
+ "acc1d2ae2cda66861054826928c25d01a2428e688cc8132a9c381831bf29ab5a"
+ ),
+ },
+ )
+ self.assertTrue(
+ RUNNER.is_device_plane_manager_release_v6_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")