diff --git a/dashboard/src/lib/components/DeviceIcon.svelte b/dashboard/src/lib/components/DeviceIcon.svelte index ac980e74..84fbd1ae 100644 --- a/dashboard/src/lib/components/DeviceIcon.svelte +++ b/dashboard/src/lib/components/DeviceIcon.svelte @@ -42,11 +42,10 @@ const NVIDIA_LOGO_PATH = "M0.81 0.429V0.299c0.013 -0.001 0.026 -0.002 0.038 -0.002 0.355 -0.011 0.588 0.306 0.588 0.306S1.186 0.952 0.916 0.952c-0.036 0 -0.071 -0.006 -0.105 -0.017V0.542c0.138 0.017 0.166 0.078 0.249 0.216l0.185 -0.155s-0.135 -0.177 -0.362 -0.177c-0.024 -0.001 -0.048 0.001 -0.072 0.003m0 -0.429v0.194l0.038 -0.002c0.494 -0.017 0.816 0.405 0.816 0.405s-0.37 0.45 -0.754 0.45c-0.034 0 -0.066 -0.003 -0.099 -0.009v0.12c0.027 0.003 0.055 0.006 0.082 0.006 0.358 0 0.618 -0.183 0.869 -0.399 0.042 0.034 0.212 0.114 0.247 0.15 -0.238 0.2 -0.794 0.361 -1.11 0.361 -0.03 0 -0.059 -0.002 -0.088 -0.005v0.169h1.362V0zm0 0.935v0.102c-0.331 -0.059 -0.423 -0.404 -0.423 -0.404s0.159 -0.176 0.423 -0.205v0.112h-0.001C0.671 0.524 0.562 0.654 0.562 0.654s0.062 0.218 0.248 0.282m-0.588 -0.316s0.196 -0.29 0.589 -0.32V0.194C0.376 0.229 0 0.597 0 0.597s0.213 0.616 0.81 0.672v-0.112c-0.438 -0.054 -0.588 -0.538 -0.588 -0.538"; - // Tux penguin paths (viewBox 0 0 100 100) - const TUX_BODY_PATH = - "M50 8c-8 0-14 6-14 13 0 4 2 8 5 10-8 4-16 14-16 28v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V59c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z"; - const TUX_BELLY_PATH = - "M38 52c0-8 5-15 12-15s12 7 12 15v14c0 4-5 7-12 7s-12-3-12-7V52z"; + // Tux penguin logo path (viewBox 0 0 100 100) + const TUX_LOGO_PATH = + "M50 5C42 5 36 11 36 18c0 4 2 8 5 10C33 32 25 42 25 56v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V56c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z"; + const TUX_LOGO_VIEWBOX = "0 0 100 100"; const wireColor = "rgba(179,179,179,0.8)"; const strokeWidth = 1.5; @@ -55,7 +54,8 @@ const isSpark = $derived( modelLower.includes("dgx") || modelLower.includes("gx10"), ); - const isLinux = $derived(!isSpark && modelLower.includes("linux")); + const isLinux = $derived(!isSpark && modelLower.startsWith("linux")); + const isLinuxLaptop = $derived(isLinux && modelLower.includes("laptop")); // ── DGX Spark dimensions ── const dgxW = $derived(size * 1.55); @@ -75,13 +75,11 @@ const dgxClipId = $derived(`di-dgx-${uid}`); const dgxTextureId = $derived(`di-dgx-tex-${uid}`); - // ── Linux Tux dimensions ── - const tuxSize = $derived(Math.min(size * 1.0, size * 1.1)); - const tuxScale = $derived(tuxSize / 100); - const tuxTx = $derived(cx - (100 * tuxScale) / 2); - const tuxTy = $derived(cy - (100 * tuxScale) / 2); - const tuxEyeR = $derived(2.5 * tuxScale); - const tuxBellyClipId = $derived(`di-tux-belly-${uid}`); + // ── Linux Desktop dimensions (reuses Mac Studio proportions) ── + const linuxDesktopClipId = $derived(`di-linux-desktop-${uid}`); + + // ── Linux Laptop dimensions (reuses MacBook proportions) ── + const linuxScreenClipId = $derived(`di-linux-screen-${uid}`); // ── Mac Studio dimensions (same ratios as TopologyGraph) ── const studioW = $derived(size * 1.25); @@ -285,98 +283,125 @@ font-weight="700">NVIDIA -{:else if isLinux} - +{:else if isLinuxLaptop} + - - + - - - - - - - {#if ramPercent > 0} - {@const bellyTop = tuxTy + 37 * tuxScale} - {@const bellyH = 36 * tuxScale} - {@const memH = (ramPercent / 100) * bellyH} {/if} - - - - - - + + {@const tuxH = mbScreenH * 0.35} + {@const tuxW = tuxH} + {@const tuxX = cx - tuxW / 2} + {@const tuxY = mbY + mbScreenH / 2 - tuxH / 2} + + + - + + +{:else if isLinux} + + + + + + - - - + {#if ramPercent > 0} + + {/if} + + + {@const dtH = (studioH - studioTopH) * 0.55} + {@const dtW = dtH} + {@const dtX = cx - dtW / 2} + {@const dtY = studioY + studioTopH + (studioH - studioTopH) / 2 - dtH / 2} + + + {:else if modelLower === "mac studio" || modelLower === "mac mini"} diff --git a/dashboard/src/lib/components/ModelCard.svelte b/dashboard/src/lib/components/ModelCard.svelte index c2498c42..9623d45c 100644 --- a/dashboard/src/lib/components/ModelCard.svelte +++ b/dashboard/src/lib/components/ModelCard.svelte @@ -988,8 +988,20 @@ stroke-width="1.5" /> - - + + - - + + - - + + {:else} diff --git a/dashboard/src/lib/components/TopologyGraph.svelte b/dashboard/src/lib/components/TopologyGraph.svelte index cc9823bf..15f8cb79 100644 --- a/dashboard/src/lib/components/TopologyGraph.svelte +++ b/dashboard/src/lib/components/TopologyGraph.svelte @@ -121,12 +121,11 @@ const NVIDIA_LOGO_PATH = "M0.81 0.429V0.299c0.013 -0.001 0.026 -0.002 0.038 -0.002 0.355 -0.011 0.588 0.306 0.588 0.306S1.186 0.952 0.916 0.952c-0.036 0 -0.071 -0.006 -0.105 -0.017V0.542c0.138 0.017 0.166 0.078 0.249 0.216l0.185 -0.155s-0.135 -0.177 -0.362 -0.177c-0.024 -0.001 -0.048 0.001 -0.072 0.003m0 -0.429v0.194l0.038 -0.002c0.494 -0.017 0.816 0.405 0.816 0.405s-0.37 0.45 -0.754 0.45c-0.034 0 -0.066 -0.003 -0.099 -0.009v0.12c0.027 0.003 0.055 0.006 0.082 0.006 0.358 0 0.618 -0.183 0.869 -0.399 0.042 0.034 0.212 0.114 0.247 0.15 -0.238 0.2 -0.794 0.361 -1.11 0.361 -0.03 0 -0.059 -0.002 -0.088 -0.005v0.169h1.362V0zm0 0.935v0.102c-0.331 -0.059 -0.423 -0.404 -0.423 -0.404s0.159 -0.176 0.423 -0.205v0.112h-0.001C0.671 0.524 0.562 0.654 0.562 0.654s0.062 0.218 0.248 0.282m-0.588 -0.316s0.196 -0.29 0.589 -0.32V0.194C0.376 0.229 0 0.597 0 0.597s0.213 0.616 0.81 0.672v-0.112c-0.438 -0.054 -0.588 -0.538 -0.588 -0.538"; - // Tux penguin SVG path — simplified silhouette for topology view - // viewBox: 0 0 100 100 - const TUX_BODY_PATH = - "M50 8c-8 0-14 6-14 13 0 4 2 8 5 10-8 4-16 14-16 28v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V59c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z"; - const TUX_BELLY_PATH = - "M38 52c0-8 5-15 12-15s12 7 12 15v14c0 4-5 7-12 7s-12-3-12-7V52z"; + // Tux penguin logo — simplified silhouette suitable as a logo overlay + // viewBox: 0 0 100 120 + const TUX_LOGO_PATH = + "M50 5C42 5 36 11 36 18c0 4 2 8 5 10C33 32 25 42 25 56v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V56c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z"; + const TUX_LOGO_VIEWBOX = "0 0 100 100"; function formatBytes(bytes: number, decimals = 1): string { if (!bytes || bytes === 0) return "0B"; @@ -567,10 +566,11 @@ const modelLower = modelId.toLowerCase(); const identity = identitiesData[nodeInfo.id]; const nameLower = (friendlyName || "").toLowerCase(); - const isSpark = - modelLower.includes("dgx") || modelLower.includes("gx10"); + const isSpark = modelLower.includes("dgx") || modelLower.includes("gx10"); const isLinux = - !isSpark && (modelLower === "linux" || identity?.osVersion === "Linux"); + !isSpark && + (modelLower.startsWith("linux") || identity?.osVersion === "Linux"); + const isLinuxLaptop = isLinux && modelLower.includes("laptop"); // Check node states for styling const isHighlighted = highlightedNodes.has(nodeInfo.id); @@ -826,107 +826,202 @@ .attr("font-family", "monospace") .attr("font-weight", "700") .text("NVIDIA"); - } else if (isLinux) { - // Linux — Tux penguin - iconBaseWidth = nodeRadius * 1.0; - iconBaseHeight = nodeRadius * 1.1; - const tuxSize = Math.min(iconBaseWidth, iconBaseHeight); - const scale = tuxSize / 100; - const tx = nodeInfo.x - (100 * scale) / 2; - const ty = nodeInfo.y - (100 * scale) / 2; + } else if (isLinuxLaptop) { + // Linux Laptop — same shape as MacBook but with Tux logo + iconBaseWidth = nodeRadius * 1.6; + iconBaseHeight = nodeRadius * 1.15; + const x = nodeInfo.x - iconBaseWidth / 2; + const y = nodeInfo.y - iconBaseHeight / 2; - // Body (dark outline) + const screenHeight = iconBaseHeight * 0.7; + const baseHeight = iconBaseHeight * 0.3; + const screenWidth = iconBaseWidth * 0.85; + const screenX = nodeInfo.x - screenWidth / 2; + const screenBezel = 3; + + const linuxScreenClipId = `linux-screen-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`; + defs + .append("clipPath") + .attr("id", linuxScreenClipId) + .append("rect") + .attr("x", screenX + screenBezel) + .attr("y", y + screenBezel) + .attr("width", screenWidth - screenBezel * 2) + .attr("height", screenHeight - screenBezel * 2) + .attr("rx", 2); + + // Screen outer frame nodeG - .append("path") + .append("rect") .attr("class", "node-outline") - .attr("d", TUX_BODY_PATH) - .attr("transform", `translate(${tx}, ${ty}) scale(${scale})`) + .attr("x", screenX) + .attr("y", y) + .attr("width", screenWidth) + .attr("height", screenHeight) + .attr("rx", 3) .attr("fill", "#1a1a1a") .attr("stroke", wireColor) - .attr("stroke-width", strokeWidth / scale); + .attr("stroke-width", strokeWidth); - // White belly + // Screen inner nodeG - .append("path") - .attr("d", TUX_BELLY_PATH) - .attr("transform", `translate(${tx}, ${ty}) scale(${scale})`) - .attr("fill", "rgba(220,220,220,0.85)"); + .append("rect") + .attr("x", screenX + screenBezel) + .attr("y", y + screenBezel) + .attr("width", screenWidth - screenBezel * 2) + .attr("height", screenHeight - screenBezel * 2) + .attr("rx", 2) + .attr("fill", "#0a0a12"); - // Memory fill on belly (from bottom up) + // Memory fill on screen if (ramUsagePercent > 0) { - const bellyClipId = `tux-belly-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`; - defs - .append("clipPath") - .attr("id", bellyClipId) - .append("path") - .attr("d", TUX_BELLY_PATH) - .attr("transform", `translate(${tx}, ${ty}) scale(${scale})`); - - const bellyTop = ty + 37 * scale; - const bellyHeight = 36 * scale; - const memHeight = (ramUsagePercent / 100) * bellyHeight; + const memFillTotalHeight = screenHeight - screenBezel * 2; + const memFillActualHeight = + (ramUsagePercent / 100) * memFillTotalHeight; nodeG .append("rect") - .attr("x", tx + 38 * scale) - .attr("y", bellyTop + bellyHeight - memHeight) - .attr("width", 24 * scale) - .attr("height", memHeight) - .attr("fill", "rgba(255,215,0,0.75)") - .attr("clip-path", `url(#${bellyClipId})`); + .attr("x", screenX + screenBezel) + .attr( + "y", + y + screenBezel + (memFillTotalHeight - memFillActualHeight), + ) + .attr("width", screenWidth - screenBezel * 2) + .attr("height", memFillActualHeight) + .attr("fill", "rgba(255,215,0,0.85)") + .attr("clip-path", `url(#${linuxScreenClipId})`); } - // Eyes - const eyeRadius = 2.5 * scale; + // Tux logo on screen + const tuxLogoH = screenHeight * 0.35; + const tuxLogoW = tuxLogoH; + const tuxLogoX = nodeInfo.x - tuxLogoW / 2; + const tuxLogoY = y + screenHeight / 2 - tuxLogoH / 2; nodeG - .append("circle") - .attr("cx", tx + 44 * scale) - .attr("cy", ty + 16 * scale) - .attr("r", eyeRadius) - .attr("fill", "white"); - nodeG - .append("circle") - .attr("cx", tx + 56 * scale) - .attr("cy", ty + 16 * scale) - .attr("r", eyeRadius) - .attr("fill", "white"); - // Pupils - nodeG - .append("circle") - .attr("cx", tx + 44 * scale) - .attr("cy", ty + 16 * scale) - .attr("r", eyeRadius * 0.5) - .attr("fill", "#1a1a1a"); - nodeG - .append("circle") - .attr("cx", tx + 56 * scale) - .attr("cy", ty + 16 * scale) - .attr("r", eyeRadius * 0.5) - .attr("fill", "#1a1a1a"); + .append("svg") + .attr("x", tuxLogoX) + .attr("y", tuxLogoY) + .attr("width", tuxLogoW) + .attr("height", tuxLogoH) + .attr("viewBox", TUX_LOGO_VIEWBOX) + .append("path") + .attr("d", TUX_LOGO_PATH) + .attr("fill", "#FFFFFF") + .attr("opacity", 0.9); + + // Keyboard base (trapezoidal) + const baseY = y + screenHeight; + const baseTopWidth = screenWidth; + const baseBottomWidth = iconBaseWidth; + const baseTopX = nodeInfo.x - baseTopWidth / 2; + const baseBottomX = nodeInfo.x - baseBottomWidth / 2; - // Beak nodeG .append("path") .attr( "d", - `M${tx + 46 * scale} ${ty + 22 * scale} L${tx + 50 * scale} ${ty + 27 * scale} L${tx + 54 * scale} ${ty + 22 * scale} Z`, + `M ${baseTopX} ${baseY} L ${baseTopX + baseTopWidth} ${baseY} L ${baseBottomX + baseBottomWidth} ${baseY + baseHeight} L ${baseBottomX} ${baseY + baseHeight} Z`, ) - .attr("fill", "#E8A317"); + .attr("fill", "#2c2c2c") + .attr("stroke", wireColor) + .attr("stroke-width", 1); - // Feet + // Keyboard area + const keyboardX = baseTopX + 6; + const keyboardY = baseY + 3; + const keyboardWidth = baseTopWidth - 12; + const keyboardHeight = baseHeight * 0.55; nodeG - .append("ellipse") - .attr("cx", tx + 42 * scale) - .attr("cy", ty + 94 * scale) - .attr("rx", 6 * scale) - .attr("ry", 2.5 * scale) - .attr("fill", "#E8A317"); + .append("rect") + .attr("x", keyboardX) + .attr("y", keyboardY) + .attr("width", keyboardWidth) + .attr("height", keyboardHeight) + .attr("fill", "rgba(0,0,0,0.2)") + .attr("rx", 2); + + // Trackpad + const trackpadWidth = baseTopWidth * 0.4; + const trackpadX = nodeInfo.x - trackpadWidth / 2; + const trackpadY = baseY + keyboardHeight + 5; + const trackpadHeight = baseHeight * 0.3; nodeG - .append("ellipse") - .attr("cx", tx + 58 * scale) - .attr("cy", ty + 94 * scale) - .attr("rx", 6 * scale) - .attr("ry", 2.5 * scale) - .attr("fill", "#E8A317"); + .append("rect") + .attr("x", trackpadX) + .attr("y", trackpadY) + .attr("width", trackpadWidth) + .attr("height", trackpadHeight) + .attr("fill", "rgba(255,255,255,0.08)") + .attr("rx", 2); + } else if (isLinux) { + // Linux Desktop — same shape as Mac Studio but with Tux logo + iconBaseWidth = nodeRadius * 1.25; + iconBaseHeight = nodeRadius * 0.85; + const x = nodeInfo.x - iconBaseWidth / 2; + const y = nodeInfo.y - iconBaseHeight / 2; + const cornerRadius = 4; + const topSurfaceHeight = iconBaseHeight * 0.15; + + const linuxDesktopClipId = `linux-desktop-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`; + defs + .append("clipPath") + .attr("id", linuxDesktopClipId) + .append("rect") + .attr("x", x) + .attr("y", y + topSurfaceHeight) + .attr("width", iconBaseWidth) + .attr("height", iconBaseHeight - topSurfaceHeight) + .attr("rx", cornerRadius - 1); + + // Main body + nodeG + .append("rect") + .attr("class", "node-outline") + .attr("x", x) + .attr("y", y) + .attr("width", iconBaseWidth) + .attr("height", iconBaseHeight) + .attr("rx", cornerRadius) + .attr("fill", "#1a1a1a") + .attr("stroke", wireColor) + .attr("stroke-width", strokeWidth); + + // Memory fill + if (ramUsagePercent > 0) { + const memFillTotalHeight = iconBaseHeight - topSurfaceHeight; + const memFillActualHeight = + (ramUsagePercent / 100) * memFillTotalHeight; + nodeG + .append("rect") + .attr("x", x) + .attr( + "y", + y + topSurfaceHeight + (memFillTotalHeight - memFillActualHeight), + ) + .attr("width", iconBaseWidth) + .attr("height", memFillActualHeight) + .attr("fill", "rgba(255,215,0,0.75)") + .attr("clip-path", `url(#${linuxDesktopClipId})`); + } + + // Tux logo centered on front face + const tuxLogoH = (iconBaseHeight - topSurfaceHeight) * 0.55; + const tuxLogoW = tuxLogoH; + const tuxLogoX = nodeInfo.x - tuxLogoW / 2; + const tuxLogoY = + y + + topSurfaceHeight + + (iconBaseHeight - topSurfaceHeight) / 2 - + tuxLogoH / 2; + nodeG + .append("svg") + .attr("x", tuxLogoX) + .attr("y", tuxLogoY) + .attr("width", tuxLogoW) + .attr("height", tuxLogoH) + .attr("viewBox", TUX_LOGO_VIEWBOX) + .append("path") + .attr("d", TUX_LOGO_PATH) + .attr("fill", "rgba(255,255,255,0.6)"); } else if (modelLower === "mac studio") { // Mac Studio - classic cube with memory fill iconBaseWidth = nodeRadius * 1.25; diff --git a/src/exo/utils/info_gatherer/system_info.py b/src/exo/utils/info_gatherer/system_info.py index ad8a6754..2e9f619c 100644 --- a/src/exo/utils/info_gatherer/system_info.py +++ b/src/exo/utils/info_gatherer/system_info.py @@ -170,7 +170,20 @@ async def _get_linux_model_and_chip() -> tuple[str, str]: pass return (model, chip) - # Generic Linux — use /proc/cpuinfo for chip + # Generic Linux — detect laptop vs desktop via chassis_type + # SMBIOS chassis types: 8,9,10,14,31,32 = portable/laptop + chassis_type = _read_dmi_field("chassis_type") + laptop_chassis_types = {"8", "9", "10", "14", "31", "32"} + if chassis_type in laptop_chassis_types: + model = "Linux Laptop" + elif chassis_type is not None: + model = "Linux Desktop" + + # Also check for battery as a fallback laptop indicator + if model == "Linux" and Path("/sys/class/power_supply/BAT0").exists(): + model = "Linux Laptop" + + # Use /proc/cpuinfo for chip cpuinfo_path = Path("/proc/cpuinfo") if cpuinfo_path.exists(): try: