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")