diff --git a/apps/device-manager/server/device-manager-presentation.test.mjs b/apps/device-manager/server/device-manager-presentation.test.mjs index e4bcc22..f426d65 100644 --- a/apps/device-manager/server/device-manager-presentation.test.mjs +++ b/apps/device-manager/server/device-manager-presentation.test.mjs @@ -61,6 +61,29 @@ test("production Compose provides exact persistent writable presentation storage assert.ok(!client.includes("#b9ff4a")); }); +test("Device Core overview follows the Mission Core landing-stage geometry", async () => { + const styles = await readFile( + new URL("../src/styles.css", import.meta.url), + "utf8", + ); + for (const expected of [ + ".device-manager-stage {\n height: 100%;\n overflow: hidden;", + "left: clamp(2rem, 5vw, 6rem);", + "width: min(39rem, 50%);", + "transform: translateY(-55%);", + "linear-gradient(90deg, rgb(5 6 8 / 0.82) 0%, rgb(5 6 8 / 0.54) 46%, rgb(5 6 8 / 0.24) 100%)", + "font-size: clamp(3rem, 7vw, 7.6rem);", + ]) assert.ok(styles.includes(expected), expected); + assert.ok(!/\.device-manager-home\s*\{[^}]*max-width:\s*1420px/s.test(styles)); + const client = await readFile( + new URL("../src/DeviceManagerApp.tsx", import.meta.url), + "utf8", + ); + assert.ok(client.includes('className="device-manager-home"')); + assert.ok(client.includes('data-has-media={hasMedia ? "true" : undefined}')); + assert.ok(!client.includes(' { const environment = normalizeEnvironmentPresentation({ defaultTeaser: { diff --git a/apps/device-manager/src/DeviceManagerApp.tsx b/apps/device-manager/src/DeviceManagerApp.tsx index f7a084e..9ffbb67 100644 --- a/apps/device-manager/src/DeviceManagerApp.tsx +++ b/apps/device-manager/src/DeviceManagerApp.tsx @@ -684,6 +684,9 @@ function DeviceStage({ && primarySections.some((section) => section.value === value) && values.indexOf(value) === index )); + const hasMedia = overview.background.enabled && overview.background.items.some( + (item) => Boolean(mediaSource(item)) && Boolean(item.mediaKind), + ); return (
{error ? ( @@ -694,7 +697,10 @@ function DeviceStage({ ) : null} - +
) : null}
-
+
); } diff --git a/apps/device-manager/src/styles.css b/apps/device-manager/src/styles.css index 02726f3..4ebfd26 100644 --- a/apps/device-manager/src/styles.css +++ b/apps/device-manager/src/styles.css @@ -257,46 +257,56 @@ body { .device-manager-stage { height: 100%; - overflow: auto; - padding: clamp(1rem, 2.5vw, 2.5rem); - scrollbar-width: thin; + overflow: hidden; } .device-manager-home { position: relative; - display: flex; - min-height: calc(100% - 1px); - max-width: 1420px; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; overflow: hidden; - flex-direction: column; - justify-content: center; - gap: clamp(3rem, 7vh, 6rem); + border-radius: var(--nodedc-radius-card); + background: var(--nodedc-canvas); isolation: isolate; - margin: 0 auto; - padding: clamp(2.5rem, 6vw, 6rem) !important; } .device-manager-home__media, .device-manager-home__scrim { position: absolute; - z-index: -2; inset: 0; width: 100%; height: 100%; } .device-manager-home__media { + z-index: 0; + display: block; object-fit: cover; } .device-manager-home__scrim { - z-index: -1; - background: var(--nodedc-overlay-bg); - opacity: 0.82; + z-index: 1; + background: + linear-gradient(90deg, rgb(5 6 8 / 0.82) 0%, rgb(5 6 8 / 0.54) 46%, rgb(5 6 8 / 0.24) 100%), + linear-gradient(0deg, rgb(5 6 8 / 0.58), transparent 38%); + pointer-events: none; +} + +.device-manager-home:not([data-has-media="true"]) .device-manager-home__scrim { + background: + radial-gradient(circle at 68% 42%, rgb(255 255 255 / 0.035), transparent 34%), + linear-gradient(90deg, rgb(5 6 8 / 0.2), transparent 62%); } .device-manager-hero { - max-width: 900px; + position: absolute; + z-index: 2; + top: 50%; + left: clamp(2rem, 5vw, 6rem); + width: min(39rem, 50%); + transform: translateY(-55%); } .device-manager-hero__icon { @@ -316,27 +326,33 @@ body { } .device-manager-hero h1 { - margin: 0.7rem 0 0.9rem; + margin: 0.75rem 0 1rem; color: var(--nodedc-text-primary); - font-size: clamp(4rem, 8vw, 8.5rem); - font-weight: 520; - letter-spacing: -0.065em; - line-height: 0.9; + font-size: clamp(3rem, 7vw, 7.6rem); + font-weight: 600; + letter-spacing: -0.072em; + line-height: 0.87; } .device-manager-hero p { - max-width: 680px; + max-width: 34rem; margin: 0; color: var(--nodedc-text-secondary); - font-size: clamp(0.96rem, 1.4vw, 1.14rem); - line-height: 1.55; + font-size: clamp(0.8rem, 1vw, 1rem); + line-height: 1.6; } .device-manager-hero__actions { display: flex; flex-wrap: wrap; - gap: 0.75rem; - margin-top: 1.5rem; + gap: 0.65rem; + margin-top: 1.6rem; +} + +@media (max-width: 1480px) { + .device-manager-hero { + width: min(35rem, 54%); + } } .environment-settings { @@ -948,20 +964,36 @@ body { gap: 1rem; } +@media (max-height: 720px) and (min-width: 761px) { + .device-manager-hero h1 { + font-size: clamp(2.7rem, 6vw, 5rem); + } + + .device-manager-hero p { + font-size: 0.76rem; + } + + .device-manager-hero__actions { + margin-top: 1rem; + } +} + @media (max-width: 760px) { body { overflow: auto; } .device-manager-stage { - padding: 1rem; + overflow: hidden; } - .device-manager-home { - min-height: auto; - padding: 2.5rem 1.25rem !important; + .device-manager-hero { + top: 42%; + right: 1.2rem; + left: 1.2rem; + width: auto; } .device-manager-hero h1 { - font-size: clamp(3.4rem, 17vw, 6rem); + font-size: clamp(2.8rem, 14vw, 5rem); } .device-manager-panel-toolbar, diff --git a/deployment/device-manager-release-v5.json b/deployment/device-manager-release-v5.json new file mode 100644 index 0000000..a8d45fe --- /dev/null +++ b/deployment/device-manager-release-v5.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": "nodedc.device-plane.device-manager-release.v5", + "releaseId": "__PATCH_ID__", + "action": "upgrade", + "predecessor": { + "kind": "release", + "patchId": "device-manager-release-v4-20260822-033", + "artifactSha256": "52ba322042f1e4f595bbfea99f8bb35630b15984e0da648dc55348bc9e5b2066" + }, + "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", + "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 1798929..19577f4 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,25 @@ 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-v3-20260812-026", ...extra] = process.argv.slice(2); +const [patchId = "device-manager-release-v5-20260822-034", ...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-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-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 isManagerOnly = isV3 || isV4; +const isV5 = descriptorPath.endsWith("release-v5.json"); +const isPersistent = isV4 || isV5; +const isManagerOnly = isV3 || isPersistent; const composeSource = resolve(devicePlaneRoot, "docker-compose.device-manager.yml"); const composeSourceSha256 = createHash("sha256").update(await readFile(composeSource)).digest("hex"); -if (!isV4 && composeSourceSha256 !== "4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e") { +if (!isPersistent && composeSourceSha256 !== "4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e") { throw new Error("historical_device_manager_compose_has_advanced"); } const entries = isManagerOnly ? [ @@ -120,7 +124,7 @@ try { "NODEDC_LAUNCHER_INTERNAL_TOKEN_FILE: /run/nodedc-secrets/device-core-internal-token", "NODEDC_DEVICE_CORE_TOKEN_FILE: /run/nodedc-secrets/management-core-token", "name: nodedc-platform_edge", - ...(isV4 ? [ + ...(isPersistent ? [ "NODEDC_DEVICE_MANAGER_PRESENTATION_PATH: /var/lib/nodedc-device-manager/device-manager-presentation.json", "NODEDC_DEVICE_MANAGER_MEDIA_ROOT: /var/lib/nodedc-device-manager/media", "source: /volume1/docker/nodedc-device-plane/data/device-manager", @@ -152,15 +156,40 @@ try { || !/^[A-Za-z0-9._-]{1,96}$/.test(predecessor.patchId || "") || !/^[a-f0-9]{64}$/.test(predecessor.artifactSha256 || "") || (descriptor.action === "activate") !== (predecessor.kind === "reconciliation") - || descriptor.healthGate !== (isV4 + || descriptor.healthGate !== (isPersistent ? "bounded-container-grace+core-contract+persistent-data" : "bounded-container-grace+core-contract") - || descriptor.rollback !== (isV4 + || descriptor.rollback !== (isPersistent ? "restore-preapply-snapshot-preserve-manager-data" : "restore-preapply-snapshot") ); if (commonContractInvalid) throw new Error("device_manager_activation_successor_contract_mismatch"); - if (descriptorPath.endsWith("release-v4.json")) { + if (descriptorPath.endsWith("release-v5.json")) { + if ( + descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v5" + || descriptor.predecessor?.kind !== "release" + || descriptor.predecessor?.patchId !== "device-manager-release-v4-20260822-033" + || descriptor.predecessor?.artifactSha256 !== "52ba322042f1e4f595bbfea99f8bb35630b15984e0da648dc55348bc9e5b2066" + || 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.rollback !== "restore-preapply-snapshot-preserve-manager-data" + || descriptor.gelios !== "untouched-legacy-only" + ) throw new Error("device_manager_v5_overview_layout_contract_mismatch"); + } else if (descriptorPath.endsWith("release-v4.json")) { if ( descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v4" || descriptor.commandTransport !== "typed-service-ping-v1" 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 56efb5d..240d8ed 100644 --- a/infra/deploy-runner/test_device_manager_control_plane_artifacts.py +++ b/infra/deploy-runner/test_device_manager_control_plane_artifacts.py @@ -448,6 +448,61 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): ) ) + def test_device_manager_release_v5_matches_mission_core_landing_geometry(self): + patch_id = "device-manager-release-v5-unit-001" + manifest, entries, names, result = self.assert_deterministic_artifact( + "build-device-manager-control-plane-artifact.mjs", + patch_id, + RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_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)) + template = json.loads( + ( + DEVICE_CORE_ROOT + / "deployment/device-manager-release-v5.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.v5" + ), + boundaries=( + RUNNER.expected_device_plane_manager_release_v5_boundaries() + ), + expected_release_id=patch_id, + ), + descriptor, + ) + self.assertEqual( + descriptor["overviewLayout"], + "mission-core-landing-stage-v1", + ) + self.assertEqual( + descriptor["predecessor"], + { + "kind": "release", + "patchId": "device-manager-release-v4-20260822-033", + "artifactSha256": ( + "52ba322042f1e4f595bbfea99f8bb35630b15984e0da648dc55348bc9e5b2066" + ), + }, + ) + self.assertTrue( + RUNNER.is_device_plane_manager_release_v5_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")