[object Object]

← back to Exo

Reinstate dashboard

3f192f20cc9cb6d93ab0457369a361919dabad0a · 2025-07-28 15:18:23 -0700 · Seth Howes

Files touched

Diff

commit 3f192f20cc9cb6d93ab0457369a361919dabad0a
Author: Seth Howes <71157822+sethhowes@users.noreply.github.com>
Date:   Mon Jul 28 15:18:23 2025 -0700

    Reinstate dashboard
---
 dashboard/index.html | 1411 ++++++++++++++++++++++++++++++++++++++++++++++++++
 master/api.py        |   33 +-
 2 files changed, 1422 insertions(+), 22 deletions(-)

diff --git a/dashboard/index.html b/dashboard/index.html
new file mode 100644
index 00000000..9d8c9e9a
--- /dev/null
+++ b/dashboard/index.html
@@ -0,0 +1,1411 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>EXO</title>
+    <style>
+        :root {
+            --exo-black: #121212;
+            --exo-dark-gray: #1E1E1E;
+            --exo-medium-gray: #2C2C2C;
+            --exo-light-gray: #B3B3B3;
+            --exo-yellow: #FFD700;
+            --exo-yellow-darker: #cca300;
+            --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+        }
+
+        body {
+            background-color: var(--exo-black);
+            color: var(--exo-light-gray);
+            font-family: var(--font-family);
+            margin: 0;
+            padding: 20px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+        }
+
+        .dashboard-header {
+            width: 100%;
+            max-width: 1200px;
+            margin-bottom: 30px;
+            text-align: left;
+        }
+
+        .dashboard-header h1 {
+            font-size: 2.5em;
+            color: var(--exo-yellow);
+            margin: 0;
+            font-weight: 600;
+        }
+        .dashboard-header h1 .logo-text {
+            font-weight: bold;
+        }
+        .dashboard-header p {
+            font-size: 1em;
+            color: var(--exo-light-gray);
+            margin-top: 5px;
+        }
+        .dashboard-header .last-updated {
+            font-size: 0.8em;
+            color: var(--exo-medium-gray);
+            margin-top: 10px;
+        }
+
+        /* Removed .node-grid and related card styles as we move to SVG graph */
+        /* Styles for the new topology graph */
+        #topologyGraphContainer {
+            width: 100%;
+            max-width: 1200px; /* Max width consistent with header */
+            height: 70vh; /* Viewport height, can be adjusted */
+            min-height: 500px; /* Minimum height */
+            background-color: var(--exo-dark-gray); /* Dark background for the graph area */
+            border-radius: 8px;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+            margin-top: 20px;
+            position: relative; /* For potential absolute positioning of elements within */
+        }
+
+        .graph-node circle {
+            stroke: var(--exo-medium-gray);
+            stroke-width: 1.5px;
+            fill: var(--exo-medium-gray);
+            cursor: pointer;
+            transition: fill 0.2s ease, stroke-width 0.2s ease;
+        }
+        .graph-node circle:hover {
+            fill: var(--exo-yellow-darker);
+            stroke-width: 3px;
+        }
+        .graph-node text {
+            fill: var(--exo-light-gray);
+            font-size: 10px;
+            text-anchor: middle;
+            pointer-events: none; /* So text doesn't interfere with circle hover */
+            font-family: var(--font-family);
+        }
+        .graph-node .node-info-text {
+            font-size: 8px;
+            fill: var(--exo-light-gray);
+            text-anchor: middle;
+            pointer-events: none;
+        }
+        .graph-node .node-memory-text {
+            font-size: 15px;
+            fill: #FFFFFF;
+            text-anchor: middle;
+            pointer-events: none;
+            font-weight: 500;
+        }
+        .graph-node .gpu-temp-on-bar {
+            font-size: 16px; /* Increased from 14px */
+            fill: #FFFFFF; /* Changed to white */
+            text-anchor: middle;
+            dominant-baseline: central;
+            pointer-events: none;
+            font-weight: bold;
+        }
+        .graph-link {
+            stroke: var(--exo-light-gray);
+            stroke-width: 1px;
+            opacity: 0.8;
+            stroke-dasharray: 4, 4; /* 4px dash, 4px gap */
+            animation: flowAnimation 0.75s linear infinite;
+        }
+
+        @keyframes flowAnimation {
+            from {
+                stroke-dashoffset: 0;
+            }
+            to {
+                stroke-dashoffset: -8; /* Negative of (dash + gap) for forward flow */
+            }
+        }
+
+        .node-info-grid {
+            display: grid;
+            grid-template-columns: repeat(2, 1fr);
+            gap: 10px 15px;
+        }
+
+        .info-item {
+            font-size: 0.9em;
+        }
+
+        .info-item .label {
+            display: block;
+            font-size: 0.8em;
+            color: var(--exo-light-gray);
+            margin-bottom: 3px;
+            opacity: 0.7;
+        }
+
+        .info-item .value {
+            font-weight: 500;
+            color: #E0E0E0;
+        }
+
+        .info-item .value.accent {
+            color: var(--exo-yellow);
+            font-weight: bold;
+        }
+
+        .progress-bar-container {
+            width: 100%;
+            background-color: var(--exo-medium-gray);
+            border-radius: 4px;
+            height: 10px;
+            overflow: hidden;
+            margin-top: 4px;
+        }
+
+        .progress-bar {
+            height: 100%;
+            background-color: var(--exo-yellow);
+            border-radius: 4px;
+            transition: width 0.3s ease; /* Keep transition for smoothness */
+        }
+
+        .node-footer {
+            font-size: 0.8em;
+            color: var(--exo-light-gray);
+            opacity: 0.6;
+            margin-top: auto;
+            padding-top: 10px;
+            border-top: 1px solid var(--exo-medium-gray);
+            text-align: right;
+        }
+
+        .loading-message, .error-message {
+            text-align: center;
+            font-size: 1.2em;
+            padding: 20px;
+            color: var(--exo-light-gray);
+        }
+        .error-message {
+            color: #F44336;
+        }
+
+        /* Styles for Node Detail Panel */
+        #nodeDetailPanel {
+            position: fixed;
+            right: -400px; /* Start off-screen */
+            top: 0;
+            width: 380px;
+            height: 100vh;
+            background-color: var(--exo-dark-gray);
+            box-shadow: -5px 0px 15px rgba(0,0,0,0.3);
+            padding: 20px;
+            overflow-y: auto;
+            transition: right 0.3s ease-in-out;
+            z-index: 1000;
+            border-left: 1px solid var(--exo-medium-gray);
+        }
+        #nodeDetailPanel.visible {
+            right: 0;
+        }
+        #nodeDetailPanel h2 {
+            color: var(--exo-yellow);
+            margin-top: 0;
+            margin-bottom: 5px;
+            font-size: 1.6em;
+        }
+        #nodeDetailPanel .detail-node-id {
+            font-size: 0.7em;
+            color: var(--exo-medium-gray);
+            margin-bottom: 15px;
+            word-break: break-all;
+            opacity: 0.7;
+        }
+        #nodeDetailPanel .info-item {
+            margin-bottom: 12px;
+            font-size: 0.95em;
+        }
+        #nodeDetailPanel .info-item .label {
+            display: block;
+            font-size: 0.8em;
+            color: var(--exo-light-gray);
+            margin-bottom: 4px;
+            opacity: 0.7;
+        }
+        #nodeDetailPanel .info-item .value {
+            font-weight: 500;
+            color: #E0E0E0;
+        }
+        #nodeDetailPanel .info-item .value.accent {
+            color: var(--exo-yellow);
+            font-weight: bold;
+        }
+        #nodeDetailPanel .progress-bar-container {
+            width: 100%;
+            background-color: var(--exo-medium-gray);
+            border-radius: 4px;
+            height: 10px;
+            overflow: hidden;
+            margin-top: 4px;
+        }
+        #nodeDetailPanel .progress-bar {
+            height: 100%;
+            background-color: var(--exo-yellow);
+            border-radius: 4px;
+            transition: width 0.3s ease;
+        }
+        #closeDetailPanel {
+            position: absolute;
+            top: 15px;
+            right: 15px;
+            font-size: 1.5em;
+            color: var(--exo-light-gray);
+            cursor: pointer;
+            padding: 5px;
+            line-height: 1;
+        }
+        #closeDetailPanel:hover {
+            color: var(--exo-yellow);
+        }
+
+    </style>
+</head>
+<body>
+
+    <div class="dashboard-header">
+        <h1><span class="logo-text">EXO</span></h1>
+        <p class="last-updated" id="lastUpdated">Fetching data...</p>
+    </div>
+
+    <!-- Replaced node-grid with SVG container for topology graph -->
+    <svg id="topologyGraphContainer"></svg>
+
+    <div id="nodeDetailPanel">
+        <span id="closeDetailPanel" title="Close">&times;</span>
+        <h2 id="detailFriendlyName">Node Details</h2>
+        <div id="detailNodeId" class="detail-node-id"></div>
+        <div id="detailContent">
+            <!-- Info items will be populated here -->
+        </div>
+    </div>
+
+    <script>
+        // Helper to convert RGB string for box-shadow
+        function setRgbVar(cssVarName, hexColor) {
+            const r = parseInt(hexColor.slice(1, 3), 16);
+            const g = parseInt(hexColor.slice(3, 5), 16);
+            const b = parseInt(hexColor.slice(5, 7), 16);
+            document.documentElement.style.setProperty(`--${cssVarName}-rgb`, `${r}, ${g}, ${b}`);
+        }
+        setRgbVar('exo-yellow', getComputedStyle(document.documentElement).getPropertyValue('--exo-yellow').trim());
+
+        const topologyGraphContainer = document.getElementById('topologyGraphContainer');
+        const lastUpdatedElement = document.getElementById('lastUpdated');
+        const nodeDetailPanel = document.getElementById('nodeDetailPanel');
+        const closeDetailPanelButton = document.getElementById('closeDetailPanel');
+        const detailFriendlyName = document.getElementById('detailFriendlyName');
+        const detailNodeId = document.getElementById('detailNodeId');
+        const detailContent = document.getElementById('detailContent');
+
+        const USE_MOCK_DATA = false; // <<< FLAG TO TOGGLE MOCK DATA
+        let currentlySelectedNodeId = null; // To store the ID of the currently selected node
+
+        const API_ENDPOINT = window.location.origin + window.location.pathname.replace(/\/$/, "") + '/state';
+        const REFRESH_INTERVAL = 1000; // 1 second
+
+        // SVG Path for Apple Logo (scaled to roughly 20x24 units)
+        // const APPLE_LOGO_PATH = "M15.36,10.08c0-2.84-1.92-4.48-4.8-4.48-2.36,0-4.44,1.44-5.64,2.84-1.48,1.68-2.44,4-2.44,6.52,0,2.92,1.72,4.6,4.76,4.6,2.48,0,4.64-1.52,5.84-2.96,1.36-1.6,2.32-3.96,2.32-6.52ZM11.48,0c2.4,0,4.24.48,5.48,1.44a5.07,5.07,0,0,1,1.44,3.88c0,2.04-.8,3.56-2.44,4.6-1.68,1.08-3.48,1.6-5.44,1.6-.96,0-2.12-.12-3.48-.32V3.08A11.29,11.29,0,0,1,11.48,0Z M9.04,13.2A5.31,5.31,0,0,0,11.4,14c1.6,0,2.68-.6,2.68-1.8,0-.92-.64-1.48-1.92-1.68l-1.6-.24c-1.8-.28-2.92-1.08-2.92-2.56,0-1.4,1.04-2.44,3-2.44a4.37,4.37,0,0,1,2.88.88l.88-1.72a5.73,5.73,0,0,0-3.8-1.28c-2.48,0-4.04.96-4.04,2.96,0,1.16.68,1.92,2,2.24l1.64.32c2.12.36,3.24,1.12,3.24,2.68,0,1.64-1.24,2.72-3.4,2.72a5.25,5.25,0,0,1-3.48-1.2Z";
+        // Replaced with accurate path data from SVG file
+        const APPLE_LOGO_PATH_SIMPLE = "M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z";
+
+        // Native dimensions of the logo path approx
+        const LOGO_NATIVE_WIDTH = 814;
+        const LOGO_NATIVE_HEIGHT = 1000;
+
+        // Helper function to describe an SVG arc (pie slice)
+        function describePieSlice(cx, cy, radius, startAngleDeg, endAngleDeg) {
+            const startAngleRad = (startAngleDeg - 90) * Math.PI / 180.0; // Subtract 90 to start from top
+            const endAngleRad = (endAngleDeg - 90) * Math.PI / 180.0;
+            const largeArcFlag = endAngleDeg - startAngleDeg <= 180 ? "0" : "1";
+
+            const startX = cx + radius * Math.cos(startAngleRad);
+            const startY = cy + radius * Math.sin(startAngleRad);
+            const endX = cx + radius * Math.cos(endAngleRad);
+            const endY = cy + radius * Math.sin(endAngleRad);
+
+            // For a full circle, a single arc won't work perfectly due to start/end points being the same.
+            // So, if it's very close to a full circle, we make it a tiny bit less to ensure it renders.
+            if (Math.abs(endAngleDeg - startAngleDeg) >= 359.99) {
+                 // Draw two semi-circles instead for a full circle effect if needed,
+                 // or simply use a circle element. For now, let's cap it.
+                 // This pie slice is for partial fill, so full isn't the primary goal here.
+            }
+
+
+            const d = [
+                "M", cx, cy, // Move to center
+                "L", startX, startY, // Line to start of arc
+                "A", radius, radius, 0, largeArcFlag, 1, endX, endY, // Arc
+                "Z" // Close path (back to center)
+            ].join(" ");
+
+            return d;
+        }
+
+        // Helper function to determine GPU bar color based on temperature (continuous gradient)
+        function getGpuBarColor(temp) {
+            if (isNaN(temp) || temp === null) return 'var(--exo-light-gray)'; // Default for N/A temp
+
+            const coolTemp = 45; // Temp for pure blue (Changed from 30)
+            const midTemp = 57.5;  // Temp for pure yellow (approx mid of 40-65)
+            const hotTemp = 75;  // Temp for pure red
+
+            const coolColor = { r: 93, g: 173, b: 226 };  // #5DADE2 (Blue)
+            const midColor = { r: 255, g: 215, b: 0 };   // var(--exo-yellow) #FFD700
+            const hotColor = { r: 244, g: 67, b: 54 };    // #F44336 (Red)
+
+            let r, g, b;
+
+            if (temp <= coolTemp) {
+                ({ r, g, b } = coolColor);
+            } else if (temp > coolTemp && temp <= midTemp) {
+                const ratio = (temp - coolTemp) / (midTemp - coolTemp);
+                r = Math.round(coolColor.r * (1 - ratio) + midColor.r * ratio);
+                g = Math.round(coolColor.g * (1 - ratio) + midColor.g * ratio);
+                b = Math.round(coolColor.b * (1 - ratio) + midColor.b * ratio);
+            } else if (temp > midTemp && temp < hotTemp) {
+                const ratio = (temp - midTemp) / (hotTemp - midTemp);
+                r = Math.round(midColor.r * (1 - ratio) + hotColor.r * ratio);
+                g = Math.round(midColor.g * (1 - ratio) + hotColor.g * ratio);
+                b = Math.round(midColor.b * (1 - ratio) + hotColor.b * ratio);
+            } else { // temp >= hotTemp
+                ({ r, g, b } = hotColor);
+            }
+
+            return `rgb(${r}, ${g}, ${b})`;
+        }
+
+        function formatBytes(bytes, decimals = 2) {
+            if (!bytes || bytes === 0) return '0 Bytes';
+            const k = 1024;
+            const dm = decimals < 0 ? 0 : decimals;
+            const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+            const i = Math.floor(Math.log(bytes) / Math.log(k));
+            return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+        }
+
+        function timeSince(timestampSeconds) {
+            const now = Date.now() / 1000;
+            const secondsPast = Math.floor(now - timestampSeconds);
+
+            if (secondsPast < 5) {
+                return 'just now';
+            }
+            if (secondsPast < 60) {
+                return `${secondsPast}s ago`;
+            }
+            if (secondsPast < 3600) {
+                return `${Math.floor(secondsPast / 60)}m ago`;
+            }
+            if (secondsPast <= 86400) {
+                return `${Math.floor(secondsPast / 3600)}h ago`;
+            }
+            const days = Math.floor(secondsPast / 86400);
+            return days + (days === 1 ? ' day ago' : ' days ago');
+        }
+
+        function renderNodes(nodesData) {
+            if (!topologyGraphContainer) return;
+            topologyGraphContainer.innerHTML = ''; // Clear previous SVG content
+
+            const nodeIds = Object.keys(nodesData);
+
+            if (nodeIds.length === 0) {
+                const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+                textEl.setAttribute('x', '50%');
+                textEl.setAttribute('y', '50%');
+                textEl.setAttribute('alignment-baseline', 'middle');
+                textEl.setAttribute('text-anchor', 'middle');
+                textEl.setAttribute('fill', 'var(--exo-light-gray)');
+                textEl.setAttribute('font-size', '16');
+                textEl.textContent = 'No nodes discovered yet.';
+                topologyGraphContainer.appendChild(textEl);
+                return;
+            }
+
+            const svgRect = topologyGraphContainer.getBoundingClientRect();
+            const width = svgRect.width;
+            const height = svgRect.height;
+            const centerX = width / 2;
+            const centerY = height / 2;
+            const numNodes = nodeIds.length;
+            const radius = Math.min(width, height) * 0.28; // Radius of the circle for node placement
+            const nodeRadius = 72; // Base size for overall node element, influences icon/bar sizes
+
+            const nodesWithPositions = nodeIds.map((id, index) => {
+                const angle = (index / numNodes) * 2 * Math.PI - (Math.PI / 2); // Start from top
+                return {
+                    id: id,
+                    data: nodesData[id],
+                    x: centerX + radius * Math.cos(angle),
+                    y: centerY + radius * Math.sin(angle)
+                };
+            });
+
+            // Create group for links (drawn first, so they are behind nodes)
+            const linksGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+            linksGroup.setAttribute('class', 'links-group');
+
+            for (let i = 0; i < numNodes; i++) {
+                for (let j = i + 1; j < numNodes; j++) {
+                    const link = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+                    link.setAttribute('x1', nodesWithPositions[i].x);
+                    link.setAttribute('y1', nodesWithPositions[i].y);
+                    link.setAttribute('x2', nodesWithPositions[j].x);
+                    link.setAttribute('y2', nodesWithPositions[j].y);
+                    link.setAttribute('class', 'graph-link');
+                    linksGroup.appendChild(link);
+                }
+            }
+            topologyGraphContainer.appendChild(linksGroup);
+
+            // Create group for nodes
+            const nodesGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+            nodesGroup.setAttribute('class', 'nodes-group');
+
+            nodesWithPositions.forEach(nodeInfo => {
+                const node = nodeInfo.data;
+                const nodeId = nodeInfo.id;
+
+                // --- Calculate additional metrics for display ---
+                let ramUsagePercent = 0;
+                let gpuTempDisplay = 'N/A';
+                let ramUsedFormatted = '0 Bytes';
+                let ramTotalFormatted = 'N/A';
+                let gpuUsagePercent = 0;
+                let gpuTempForColor = NaN;
+                let sysPower = 'N/A';
+
+                const macmon = node.macmon_info;
+                if (macmon) {
+                    if (macmon.memory && macmon.memory.ram_total > 0) {
+                        ramUsagePercent = (macmon.memory.ram_usage / macmon.memory.ram_total) * 100;
+                        ramUsedFormatted = formatBytes(macmon.memory.ram_usage, 1);
+                        ramTotalFormatted = formatBytes(macmon.memory.ram_total, 1);
+                    }
+                    if (macmon.temp && typeof macmon.temp.gpu_temp_avg === 'number') {
+                        let originalGpuTemp = macmon.temp.gpu_temp_avg;
+                        let clampedGpuTemp = Math.max(30, originalGpuTemp);
+                        gpuTempDisplay = `${clampedGpuTemp.toFixed(0)}°C`;
+                        gpuTempForColor = clampedGpuTemp;
+                    } else {
+                        gpuTempDisplay = 'N/A';
+                        gpuTempForColor = NaN;
+                    }
+                    if (macmon.gpu_usage && typeof macmon.gpu_usage[1] === 'number') { // Calculate GPU Usage Percentage
+                        gpuUsagePercent = macmon.gpu_usage[1] * 100;
+                    }
+                    if (macmon.sys_power) sysPower = `${macmon.sys_power.toFixed(1)} W`;
+                } else { // If no macmon_info, indicate data is missing for these
+                    ramUsagePercent = 0;
+                    gpuTempDisplay = 'N/A';
+                    ramUsedFormatted = 'N/A';
+                    ramTotalFormatted = 'N/A';
+                    gpuUsagePercent = 0;
+                    sysPower = 'N/A'; // Ensure sysPower is handled
+                }
+                // --- End Calculate additional metrics ---
+
+                let fillCol = 'var(--exo-medium-gray)'; // Default status color for the circle
+                const lastSeenSeconds = (Date.now() / 1000) - node.last_macmon_update;
+                if (lastSeenSeconds > 60 * 60) { // Over 1 hour
+                    fillCol = '#F44336'; // Error - red
+                } else if (lastSeenSeconds > 60 * 5) { // Over 5 minutes
+                    fillCol = 'var(--exo-yellow)'; // Warning - yellow
+                } else if (!node.macmon_info) {
+                    fillCol = 'var(--exo-yellow)'; // Warning if no macmon_info but seen recently
+                }
+
+
+                const nodeG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+                nodeG.setAttribute('class', 'graph-node');
+
+                const modelId = node.system_info ? node.system_info.model_id : 'Unknown';
+
+                // --- Icon Dimensions & Config ---
+                let iconBaseWidth = nodeRadius * 1.2;
+                let iconBaseHeight = nodeRadius * 1.0;
+                let specificIconGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+                const clipPathId = `clipPath-${nodeId.replace(/[^a-zA-Z0-9]/g, '-')}`;
+
+                // Add tooltip for node ID (remains on the group)
+                const titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title');
+                titleEl.textContent = `ID: ${nodeId}\nLast Seen: ${timeSince(node.last_macmon_update)}`;
+                nodeG.appendChild(titleEl);
+
+                // --- Device Specific Icon Drawing ---
+                if (modelId === "Mac Studio") {
+                    iconBaseWidth = nodeRadius * 1.25; // Slightly wider based on typical Studio proportions
+                    iconBaseHeight = nodeRadius * 0.85; // And a bit flatter than a perfect cube
+                    const x = nodeInfo.x - iconBaseWidth / 2;
+                    const y = nodeInfo.y - iconBaseHeight / 2;
+                    const cornerRadius = 4;
+                    const topSurfaceHeight = iconBaseHeight * 0.15; // Height of the top bevel/surface
+
+                    // Define ClipPath for the main front body (where memory fill goes)
+                    let defs = nodeG.querySelector('defs');
+                    if (!defs) {
+                        defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
+                        nodeG.appendChild(defs);
+                    }
+                    const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
+                    clipPath.setAttribute('id', clipPathId);
+                    const clipRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    // Clip path matches the main front body, excluding the top surface part shown visually
+                    clipRect.setAttribute('x', x);
+                    clipRect.setAttribute('y', y + topSurfaceHeight);
+                    clipRect.setAttribute('width', iconBaseWidth);
+                    clipRect.setAttribute('height', iconBaseHeight - topSurfaceHeight);
+                    clipRect.setAttribute('rx', cornerRadius -1 > 0 ? cornerRadius -1 : 0); // Slightly less rounding for internal clip if needed
+                    clipPath.appendChild(clipRect);
+                    defs.appendChild(clipPath);
+
+                    // Top Surface (like the image)
+                    const topRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    topRect.setAttribute('x', x);
+                    topRect.setAttribute('y', y);
+                    topRect.setAttribute('width', iconBaseWidth);
+                    topRect.setAttribute('height', iconBaseHeight); // Full height, front will overlay
+                    topRect.setAttribute('rx', cornerRadius);
+                    topRect.setAttribute('ry', cornerRadius);
+                    topRect.style.fill = 'var(--exo-medium-gray)'; // Color of the top surface
+                    topRect.setAttribute('stroke', 'var(--exo-light-gray)');
+                    topRect.setAttribute('stroke-width', '1px');
+                    specificIconGroup.appendChild(topRect);
+
+                    // Main Front Body (this is where memory fill will be visible)
+                    const frontBody = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    frontBody.setAttribute('x', x);
+                    frontBody.setAttribute('y', y + topSurfaceHeight);
+                    frontBody.setAttribute('width', iconBaseWidth);
+                    frontBody.setAttribute('height', iconBaseHeight - topSurfaceHeight);
+                    frontBody.style.fill = fillCol; // Node status color
+                    // No stroke for frontBody, as topRect provides the outline appearance
+                    specificIconGroup.appendChild(frontBody);
+
+                    // Memory Fill (clipped to frontBody area)
+                    if (ramUsagePercent > 0) {
+                        const memFillTotalHeight = iconBaseHeight - topSurfaceHeight;
+                        const memFillActualHeight = (ramUsagePercent / 100) * memFillTotalHeight;
+                        const memoryFillRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                        memoryFillRect.setAttribute('x', x);
+                        memoryFillRect.setAttribute('y', y + topSurfaceHeight + (memFillTotalHeight - memFillActualHeight)); // From bottom up
+                        memoryFillRect.setAttribute('width', iconBaseWidth);
+                        memoryFillRect.setAttribute('height', memFillActualHeight);
+                        let memFillColor = 'var(--exo-yellow)';
+                        if (fillCol === 'var(--exo-yellow)') memFillColor = 'var(--exo-yellow-darker)';
+                        memoryFillRect.style.fill = memFillColor;
+                        memoryFillRect.style.opacity = '0.75';
+                        memoryFillRect.setAttribute('clip-path', `url(#${clipPathId})`);
+                        specificIconGroup.appendChild(memoryFillRect);
+                    }
+
+                    // Front Panel Details (slots, LED)
+                    const detailColor = 'rgba(0,0,0,0.3)'; // Darker color for cutouts
+                    const slotHeight = iconBaseHeight * 0.12;
+                    const slotCornerRadius = 1.5;
+
+                    // Vertical slots (2)
+                    const vSlotWidth = iconBaseWidth * 0.05;
+                    const vSlotY = y + topSurfaceHeight + (iconBaseHeight - topSurfaceHeight) * 0.65 - slotHeight / 2;
+                    const vSlot1X = x + iconBaseWidth * 0.15;
+                    const vSlot2X = x + iconBaseWidth * 0.25;
+
+                    [vSlot1X, vSlot2X].forEach(vx => {
+                        const vSlot = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                        vSlot.setAttribute('x', vx - vSlotWidth / 2);
+                        vSlot.setAttribute('y', vSlotY);
+                        vSlot.setAttribute('width', vSlotWidth);
+                        vSlot.setAttribute('height', slotHeight);
+                        vSlot.setAttribute('fill', detailColor);
+                        vSlot.setAttribute('rx', slotCornerRadius);
+                        specificIconGroup.appendChild(vSlot);
+                    });
+
+                    // Horizontal slot for Mac Studio - RESTORED
+                    const hSlotWidth = iconBaseWidth * 0.2;
+                    const hSlotX = x + iconBaseWidth * 0.5 - hSlotWidth / 2;
+                    const hSlotY = vSlotY; // Align with vertical slots for simplicity
+                    const hSlot = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    hSlot.setAttribute('x', hSlotX);
+                    hSlot.setAttribute('y', hSlotY);
+                    hSlot.setAttribute('width', hSlotWidth);
+                    hSlot.setAttribute('height', slotHeight * 0.6); // Thinner horizontal slot
+                    hSlot.setAttribute('fill', detailColor);
+                    hSlot.setAttribute('rx', slotCornerRadius * 0.7);
+                    specificIconGroup.appendChild(hSlot);
+
+                    // LED indicator
+                    const ledRadius = iconBaseWidth * 0.025;
+                    const ledX = x + iconBaseWidth * 0.85;
+                    const ledY = y + topSurfaceHeight + (iconBaseHeight - topSurfaceHeight) * 0.7;
+                    const led = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+                    led.setAttribute('cx', ledX);
+                    led.setAttribute('cy', ledY);
+                    led.setAttribute('r', ledRadius);
+                    led.setAttribute('fill', 'var(--exo-light-gray)'); // Subtle LED color
+                    specificIconGroup.appendChild(led);
+
+                } else if (modelId === "Mac Mini") {
+                    iconBaseWidth = nodeRadius * 1.3;  // Mini is wide
+                    iconBaseHeight = nodeRadius * 0.7; // and quite flat
+                    const x = nodeInfo.x - iconBaseWidth / 2;
+                    const y = nodeInfo.y - iconBaseHeight / 2;
+                    const cornerRadius = 3;
+                    const topSurfaceHeight = iconBaseHeight * 0.20; // Proportionally similar top surface
+
+                    // Define ClipPath for the main front body
+                    let defs = nodeG.querySelector('defs');
+                    if (!defs) {
+                        defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
+                        nodeG.appendChild(defs);
+                    }
+                    const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
+                    clipPath.setAttribute('id', clipPathId); // Use the same clipPathId logic
+                    const clipRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    clipRect.setAttribute('x', x);
+                    clipRect.setAttribute('y', y + topSurfaceHeight);
+                    clipRect.setAttribute('width', iconBaseWidth);
+                    clipRect.setAttribute('height', iconBaseHeight - topSurfaceHeight);
+                    clipRect.setAttribute('rx', cornerRadius -1 > 0 ? cornerRadius -1 : 0);
+                    clipPath.appendChild(clipRect);
+                    defs.appendChild(clipPath);
+
+                    // Top Surface
+                    const topRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    topRect.setAttribute('x', x);
+                    topRect.setAttribute('y', y);
+                    topRect.setAttribute('width', iconBaseWidth);
+                    topRect.setAttribute('height', iconBaseHeight);
+                    topRect.setAttribute('rx', cornerRadius);
+                    topRect.setAttribute('ry', cornerRadius);
+                    topRect.style.fill = 'var(--exo-medium-gray)';
+                    topRect.setAttribute('stroke', 'var(--exo-light-gray)');
+                    topRect.setAttribute('stroke-width', '1px');
+                    specificIconGroup.appendChild(topRect);
+
+                    // Main Front Body (for memory fill)
+                    const frontBody = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    frontBody.setAttribute('x', x);
+                    frontBody.setAttribute('y', y + topSurfaceHeight);
+                    frontBody.setAttribute('width', iconBaseWidth);
+                    frontBody.setAttribute('height', iconBaseHeight - topSurfaceHeight);
+                    frontBody.style.fill = fillCol; // Node status color
+                    specificIconGroup.appendChild(frontBody);
+
+                    // Memory Fill (clipped to frontBody area)
+                    if (ramUsagePercent > 0) {
+                        const memFillTotalHeight = iconBaseHeight - topSurfaceHeight;
+                        const memFillActualHeight = (ramUsagePercent / 100) * memFillTotalHeight;
+                        const memoryFillRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                        memoryFillRect.setAttribute('x', x);
+                        memoryFillRect.setAttribute('y', y + topSurfaceHeight + (memFillTotalHeight - memFillActualHeight));
+                        memoryFillRect.setAttribute('width', iconBaseWidth);
+                        memoryFillRect.setAttribute('height', memFillActualHeight);
+                        let memFillColor = 'var(--exo-yellow)';
+                        if (fillCol === 'var(--exo-yellow)') memFillColor = 'var(--exo-yellow-darker)';
+                        memoryFillRect.style.fill = memFillColor;
+                        memoryFillRect.style.opacity = '0.75';
+                        memoryFillRect.setAttribute('clip-path', `url(#${clipPathId})`);
+                        specificIconGroup.appendChild(memoryFillRect);
+                    }
+
+                    // Front Panel Details for Mac Mini (No horizontal slot)
+                    const detailColor = 'rgba(0,0,0,0.3)';
+                    const slotHeight = iconBaseHeight * 0.18; // Adjusted for flatter mini
+                    const slotCornerRadius = 1.2;
+
+                    // Vertical slots (2)
+                    const vSlotWidth = iconBaseWidth * 0.045;
+                    const vSlotY = y + topSurfaceHeight + (iconBaseHeight - topSurfaceHeight) * 0.55 - slotHeight / 2;
+                    const vSlot1X = x + iconBaseWidth * 0.18; // Adjusted positioning for wider mini
+                    const vSlot2X = x + iconBaseWidth * 0.28;
+
+                    [vSlot1X, vSlot2X].forEach(vx => {
+                        const vSlot = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                        vSlot.setAttribute('x', vx - vSlotWidth / 2);
+                        vSlot.setAttribute('y', vSlotY);
+                        vSlot.setAttribute('width', vSlotWidth);
+                        vSlot.setAttribute('height', slotHeight);
+                        vSlot.setAttribute('fill', detailColor);
+                        vSlot.setAttribute('rx', slotCornerRadius);
+                        specificIconGroup.appendChild(vSlot);
+                    });
+
+                    // LED indicator for Mac Mini
+                    const ledRadius = iconBaseWidth * 0.022;
+                    const ledX = x + iconBaseWidth * 0.82; // Adjusted positioning
+                    const ledY = y + topSurfaceHeight + (iconBaseHeight - topSurfaceHeight) * 0.6;
+                    const led = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+                    led.setAttribute('cx', ledX);
+                    led.setAttribute('cy', ledY);
+                    led.setAttribute('r', ledRadius);
+                    led.setAttribute('fill', 'var(--exo-light-gray)');
+                    specificIconGroup.appendChild(led);
+
+                } else if (modelId === "MacBook Pro") {
+                    iconBaseWidth = nodeRadius * 1.6;  // Max width of the base
+                    iconBaseHeight = nodeRadius * 1.15; // Overall height of the open laptop visual
+                    const x = nodeInfo.x - iconBaseWidth / 2;
+                    const y = nodeInfo.y - iconBaseHeight / 2;
+
+                    const screenCornerRadius = 3;
+                    const baseCornerRadius = 2;
+                    const screenBezel = 1.5;
+
+                    const perspectiveFactor = 0.15; // How much wider the back of the base is than the front
+                    const frontWidth = iconBaseWidth * (1 - perspectiveFactor);
+                    const backWidth = iconBaseWidth;
+
+                    const screenActualHeight = iconBaseHeight * 0.70;
+                    const baseActualHeight = iconBaseHeight * 0.30;
+                    const trackpadHeight = baseActualHeight * 0.35;
+                    const keyboardAreaHeight = baseActualHeight * 0.50;
+
+                    // Define ClipPath for the screen content area
+                    let defs = nodeG.querySelector('defs');
+                    if (!defs) {
+                        defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
+                        nodeG.appendChild(defs);
+                    }
+                    const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
+                    clipPath.setAttribute('id', clipPathId);
+                    const clipRectScreen = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    clipRectScreen.setAttribute('x', x + (iconBaseWidth - frontWidth)/2 + screenBezel); // Screen aligned with front of base
+                    clipRectScreen.setAttribute('y', y + screenBezel);
+                    clipRectScreen.setAttribute('width', frontWidth - (2 * screenBezel));
+                    clipRectScreen.setAttribute('height', screenActualHeight - (2 * screenBezel));
+                    clipRectScreen.setAttribute('rx', screenCornerRadius -1 > 0 ? screenCornerRadius -1 : 0);
+                    clipPath.appendChild(clipRectScreen);
+                    defs.appendChild(clipPath);
+
+                    // Laptop Base (trapezoidal shape for perspective)
+                    const basePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+                    const baseY = y + screenActualHeight;
+                    // Recalculate coordinates for correct perspective
+                    const x_back_left = x;
+                    const x_back_right = x + backWidth;
+                    const x_front_left = x + (backWidth - frontWidth) / 2;
+                    const x_front_right = x + (backWidth + frontWidth) / 2;
+                    const y_top_base = baseY;
+                    const y_bottom_base = baseY + baseActualHeight;
+
+                    const pathData = `
+                        M ${x_front_left} ${y_top_base}
+                        L ${x_front_right} ${y_top_base}
+                        L ${x_back_right} ${y_bottom_base}
+                        L ${x_back_left} ${y_bottom_base}
+                        Z
+                    `; // Correct trapezoid path
+                    basePath.setAttribute('d', pathData.trim().replace(/\s+/g, ' '));
+                    basePath.style.fill = fillCol; // Status color for the base
+                    basePath.setAttribute('stroke', 'var(--exo-light-gray)');
+                    basePath.setAttribute('stroke-width', '1px');
+                    specificIconGroup.appendChild(basePath);
+
+                    // Keyboard Area
+                    const keyboardX = x + (iconBaseWidth - frontWidth)/2 + frontWidth * 0.05;
+                    const keyboardY = baseY + baseActualHeight * 0.05;
+                    const keyboardWidth = frontWidth * 0.9;
+                    const keyboard = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    keyboard.setAttribute('x', keyboardX);
+                    keyboard.setAttribute('y', keyboardY);
+                    keyboard.setAttribute('width', keyboardWidth);
+                    keyboard.setAttribute('height', keyboardAreaHeight);
+                    keyboard.setAttribute('fill', 'rgba(0,0,0,0.1)'); // Darker area for keyboard
+                    keyboard.setAttribute('rx', baseCornerRadius);
+                    specificIconGroup.appendChild(keyboard);
+
+                    // Trackpad Area
+                    const trackpadWidth = frontWidth * 0.4;
+                    const trackpadX = nodeInfo.x - trackpadWidth/2; // Centered
+                    const trackpadY = baseY + keyboardAreaHeight + baseActualHeight * 0.08;
+                    const trackpad = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    trackpad.setAttribute('x', trackpadX);
+                    trackpad.setAttribute('y', trackpadY);
+                    trackpad.setAttribute('width', trackpadWidth);
+                    trackpad.setAttribute('height', trackpadHeight);
+                    trackpad.setAttribute('fill', 'rgba(255,255,255,0.1)'); // Lighter area for trackpad
+                    trackpad.setAttribute('rx', baseCornerRadius);
+                    specificIconGroup.appendChild(trackpad);
+
+                    // Screen Outline & Background (drawn on top of base part that might go under it)
+                    const screenOuter = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    screenOuter.setAttribute('x', x + (iconBaseWidth - frontWidth)/2 ); // Screen aligned with front of base
+                    screenOuter.setAttribute('y', y);
+                    screenOuter.setAttribute('width', frontWidth);
+                    screenOuter.setAttribute('height', screenActualHeight);
+                    screenOuter.setAttribute('rx', screenCornerRadius);
+                    screenOuter.setAttribute('ry', screenCornerRadius);
+                    screenOuter.style.fill = 'var(--exo-dark-gray)'; // Dark screen area
+                    screenOuter.setAttribute('stroke', 'var(--exo-light-gray)');
+                    screenOuter.setAttribute('stroke-width', '1.5px');
+                    specificIconGroup.appendChild(screenOuter);
+
+                    // Memory Fill in Screen Area (Clipped)
+                    if (ramUsagePercent > 0) {
+                        const memFillTotalHeight = screenActualHeight - (2 * screenBezel);
+                        const memFillActualHeight = (ramUsagePercent / 100) * memFillTotalHeight;
+                        const memoryFillRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                        memoryFillRect.setAttribute('x', x + (iconBaseWidth - frontWidth)/2 + screenBezel);
+                        memoryFillRect.setAttribute('y', y + screenBezel + (memFillTotalHeight - memFillActualHeight)); // From bottom up
+                        memoryFillRect.setAttribute('width', frontWidth - (2 * screenBezel));
+                        memoryFillRect.setAttribute('height', memFillActualHeight);
+                        let memFillColor = 'var(--exo-yellow)';
+                        if (fillCol === 'var(--exo-yellow)') memFillColor = 'var(--exo-yellow-darker)';
+                        memoryFillRect.style.fill = memFillColor;
+                        memoryFillRect.style.opacity = '0.9';
+                        memoryFillRect.setAttribute('clip-path', `url(#${clipPathId})`);
+                        specificIconGroup.appendChild(memoryFillRect);
+                    }
+
+                    // Apple Logo on Screen (on top of memory fill)
+                    const logoPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+                    logoPath.setAttribute('d', APPLE_LOGO_PATH_SIMPLE);
+                    const targetLogoHeightMBP = screenActualHeight * 0.18;
+                    const logoScale = targetLogoHeightMBP / LOGO_NATIVE_HEIGHT; // Correct scale calculation
+                    const logoX = nodeInfo.x - (LOGO_NATIVE_WIDTH * logoScale / 2);
+                    const logoY = y + screenActualHeight / 2 - (LOGO_NATIVE_HEIGHT * logoScale / 2); // Centered Y
+                    logoPath.setAttribute('transform', `translate(${logoX} ${logoY}) scale(${logoScale})`);
+                    logoPath.setAttribute('fill', '#FFFFFF');
+                    specificIconGroup.appendChild(logoPath);
+
+                } else { // Default/Unknown
+                    // Default/Unknown Icon - A simple rounded rectangle
+                    const unknownIconWidth = iconBaseWidth * 0.8;
+                    const unknownIconHeight = iconBaseHeight * 0.8;
+                    const unknown = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    unknown.setAttribute('x', nodeInfo.x - unknownIconWidth / 2);
+                    unknown.setAttribute('y', nodeInfo.y - unknownIconHeight / 2);
+                    unknown.setAttribute('width', unknownIconWidth);
+                    unknown.setAttribute('height', unknownIconHeight);
+                    unknown.style.fill = fillCol;
+                    unknown.setAttribute('stroke', 'var(--exo-light-gray)');
+                    unknown.setAttribute('stroke-width', '1px');
+                    unknown.setAttribute('rx', '3');
+                    specificIconGroup.appendChild(unknown);
+                }
+                nodeG.appendChild(specificIconGroup);
+                // --- End Device Specific Icon Drawing ---
+
+                // --- Memory Text (Below Icon) ---
+                const nodeMemoryText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+                nodeMemoryText.setAttribute('x', nodeInfo.x);
+                nodeMemoryText.setAttribute('y', nodeInfo.y + iconBaseHeight / 2 + 14); // Position below icon, increased offset
+                nodeMemoryText.setAttribute('class', 'node-memory-text');
+                nodeMemoryText.innerHTML = `
+                    <tspan x="${nodeInfo.x}" dy="1em">${ramUsedFormatted}/${ramTotalFormatted}</tspan>
+                    <tspan x="${nodeInfo.x}" dy="1.2em">(${ramUsagePercent.toFixed(0)}%)</tspan>
+                `;
+                nodeG.appendChild(nodeMemoryText);
+
+                // --- Friendly Name Text (Above Icon) ---
+                const friendlyNameText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+                friendlyNameText.setAttribute('x', nodeInfo.x);
+                friendlyNameText.setAttribute('y', nodeInfo.y - iconBaseHeight / 2 - 15); // Position above icon
+                friendlyNameText.setAttribute('text-anchor', 'middle');
+                friendlyNameText.style.fontSize = '18px';
+                friendlyNameText.style.fill = 'var(--exo-light-gray)';
+                const friendlyName = node.friendly_name || nodeId.substring(0, 8) + '...';
+                friendlyNameText.textContent = friendlyName;
+                nodeG.appendChild(friendlyNameText);
+
+                // --- Vertical GPU Bar (Next to Icon) ---
+                const gpuVerticalBarWidth = 24; // Increased width
+                const gpuVerticalBarHeight = iconBaseHeight * 0.95; // Relative to icon height
+                const barXOffset = iconBaseWidth / 2 + 18; // Increased offset from icon's right edge
+                const gpuBarX = nodeInfo.x + barXOffset;
+                const gpuBarY = nodeInfo.y - gpuVerticalBarHeight / 2; // Centered vertically with icon
+
+                // GPU Bar Background
+                const gpuBarBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                gpuBarBg.setAttribute('x', gpuBarX);
+                gpuBarBg.setAttribute('y', gpuBarY);
+                gpuBarBg.setAttribute('width', gpuVerticalBarWidth);
+                gpuBarBg.setAttribute('height', gpuVerticalBarHeight);
+                gpuBarBg.setAttribute('fill', 'var(--exo-medium-gray)');
+                gpuBarBg.setAttribute('rx', 2);
+                nodeG.appendChild(gpuBarBg);
+
+                // GPU Bar Fill
+                if (gpuUsagePercent > 0) {
+                    const fillHeight = (gpuUsagePercent / 100) * gpuVerticalBarHeight;
+                    const gpuBarFill = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+                    gpuBarFill.setAttribute('x', gpuBarX);
+                    gpuBarFill.setAttribute('y', gpuBarY + (gpuVerticalBarHeight - fillHeight)); // Fills from bottom up
+                    gpuBarFill.setAttribute('width', gpuVerticalBarWidth);
+                    gpuBarFill.setAttribute('height', fillHeight);
+                    gpuBarFill.setAttribute('fill', getGpuBarColor(gpuTempForColor));
+                    gpuBarFill.setAttribute('rx', 2);
+                    nodeG.appendChild(gpuBarFill);
+                }
+
+                // GPU Percentage and Temperature Text on Bar (multiline)
+                const gpuInfoOnBarText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+                gpuInfoOnBarText.setAttribute('x', gpuBarX + gpuVerticalBarWidth / 2);
+                gpuInfoOnBarText.setAttribute('y', gpuBarY + gpuVerticalBarHeight / 2); // Base Y for centering
+                gpuInfoOnBarText.setAttribute('class', 'gpu-temp-on-bar');
+                gpuInfoOnBarText.style.dominantBaseline = 'central'; // Helps with overall vertical alignment
+
+                const gpuUsageText = gpuUsagePercent > 0 ? `${gpuUsagePercent.toFixed(0)}%` : '-';
+                const tempText = gpuTempDisplay === 'N/A' ? '-' : gpuTempDisplay;
+                const powerText = sysPower; // Already formatted with " W" or "N/A"
+
+                gpuInfoOnBarText.innerHTML = `
+                    <tspan x="${gpuBarX + gpuVerticalBarWidth / 2}" dy="-1.1em">${gpuUsageText}</tspan>
+                    <tspan x="${gpuBarX + gpuVerticalBarWidth / 2}" dy="1.2em">${tempText}</tspan>
+                    <tspan x="${gpuBarX + gpuVerticalBarWidth / 2}" dy="1.2em">${powerText}</tspan>
+                `;
+                nodeG.appendChild(gpuInfoOnBarText);
+
+                // Add click listener to the group
+                nodeG.addEventListener('click', () => {
+                    currentlySelectedNodeId = nodeId; // Set the globally selected node
+                    showNodeDetails(nodeId, nodesData);
+                });
+
+                nodesGroup.appendChild(nodeG);
+            });
+            topologyGraphContainer.appendChild(nodesGroup);
+        }
+
+        function showNodeDetails(selectedNodeId, allNodesData) {
+            const nodeData = allNodesData[selectedNodeId];
+            if (!nodeData) return;
+
+            detailFriendlyName.textContent = nodeData.friendly_name || selectedNodeId;
+            if (nodeData.friendly_name) {
+                detailNodeId.textContent = selectedNodeId;
+                detailNodeId.style.display = 'block';
+            } else {
+                detailNodeId.style.display = 'none';
+            }
+
+            let ramUsagePercent = 0, cpuUsagePercent = 0, gpuUsagePercent = 0;
+            let aneActive = false;
+            let sysPower = 'N/A', cpuTemp = 'N/A', gpuTemp = 'N/A';
+            let primaryIp = nodeData.addrs && nodeData.addrs.length > 0 ? nodeData.addrs[0] : 'N/A';
+            let otherIpCount = nodeData.addrs && nodeData.addrs.length > 1 ? ` (+${nodeData.addrs.length - 1})` : '';
+
+            const macmon = nodeData.macmon_info;
+            let ramUsageFormatted = '0 Bytes';
+            let ramTotalFormatted = '0 Bytes';
+
+            if (macmon) {
+                if (macmon.memory && macmon.memory.ram_total > 0) {
+                    ramUsagePercent = (macmon.memory.ram_usage / macmon.memory.ram_total) * 100;
+                    ramUsageFormatted = formatBytes(macmon.memory.ram_usage, 1);
+                    ramTotalFormatted = formatBytes(macmon.memory.ram_total, 1);
+                }
+                if (macmon.pcpu_usage && macmon.ecpu_usage) {
+                    cpuUsagePercent = ((macmon.pcpu_usage[1] + macmon.ecpu_usage[1]) / 2) * 100;
+                }
+                if (macmon.gpu_usage) {
+                    gpuUsagePercent = macmon.gpu_usage[1] * 100;
+                }
+                aneActive = macmon.ane_power > 0;
+                if (macmon.sys_power) sysPower = `${macmon.sys_power.toFixed(1)} W`;
+                if (macmon.temp) {
+                    if (macmon.temp.cpu_temp_avg) cpuTemp = `${macmon.temp.cpu_temp_avg.toFixed(1)}°C`;
+                    if (typeof macmon.temp.gpu_temp_avg === 'number') { // Check type for robustness
+                        const actualGpuTemp = macmon.temp.gpu_temp_avg;
+                        const clampedGpuTemp = Math.max(30, actualGpuTemp);
+                        gpuTemp = `${clampedGpuTemp.toFixed(1)}°C`;
+                    } // gpuTemp remains 'N/A' if not set or not a number
+                }
+            }
+
+            detailContent.innerHTML = `
+                <div class="info-item">
+                    <span class="label">IP Address</span>
+                    <span class="value">${primaryIp}${otherIpCount}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">Total Unified Memory</span>
+                    <span class="value">${formatBytes(nodeData.mem)}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">System Power</span>
+                    <span class="value">${sysPower}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">ANE</span>
+                    <span class="value ${aneActive ? 'accent' : ''}">${aneActive ? 'Active' : 'Idle'}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">CPU Temp</span>
+                    <span class="value">${cpuTemp}</span>
+                </div>
+                <div class="info-item">
+                    <span class="label">GPU Temp</span>
+                    <span class="value">${gpuTemp}</span>
+                </div>
+                <hr style="border-color: var(--exo-medium-gray); margin: 15px 0;">
+                <div class="info-item">
+                    <span class="label">Unified Memory Usage (${ramUsageFormatted} / ${ramTotalFormatted})</span>
+                    <div class="progress-bar-container">
+                        <div class="progress-bar" style="width: ${ramUsagePercent.toFixed(2)}%;"></div>
+                    </div>
+                </div>
+                <div class="info-item">
+                    <span class="label">CPU Usage (${cpuUsagePercent.toFixed(1)}%)</span>
+                    <div class="progress-bar-container">
+                        <div class="progress-bar" style="width: ${cpuUsagePercent.toFixed(2)}%;"></div>
+                    </div>
+                </div>
+                <div class="info-item">
+                    <span class="label">GPU Usage (${gpuUsagePercent.toFixed(1)}%)</span>
+                    <div class="progress-bar-container">
+                        <div class="progress-bar" style="width: ${gpuUsagePercent.toFixed(2)}%;"></div>
+                    </div>
+                </div>
+                <div class="info-item" style="margin-top: 20px; font-size:0.8em; opacity:0.7;">
+                    <span class="label">Last macmon Update</span>
+                    <span class="value">${timeSince(nodeData.last_macmon_update)} (raw: ${nodeData.last_macmon_update})</span>
+                </div>
+            `;
+
+            nodeDetailPanel.classList.add('visible');
+        }
+
+        if (closeDetailPanelButton) {
+            closeDetailPanelButton.addEventListener('click', () => {
+                nodeDetailPanel.classList.remove('visible');
+                currentlySelectedNodeId = null; // Clear selected node on manual close
+            });
+        }
+
+        let isFetching = false; // Lock to prevent multiple concurrent fetches
+        let fetchIntervalId = null;
+
+        async function fetchDataAndRender() {
+            if (isFetching) {
+                // console.log("Already fetching, skipping this interval.");
+                return;
+            }
+            isFetching = true;
+            // console.log(`Fetching data from: ${API_ENDPOINT}`); // Less verbose for 1s interval
+            if (lastUpdatedElement.textContent === 'Fetching data...' || !lastUpdatedElement.textContent.startsWith("Last updated:")) {
+                lastUpdatedElement.textContent = 'Fetching data...';
+            }
+
+            try {
+                const urlWithCacheBuster = new URL(API_ENDPOINT);
+                urlWithCacheBuster.searchParams.set('_cb', new Date().getTime()); // _cb for cache buster
+
+                const response = await fetch(urlWithCacheBuster.toString(), { cache: 'no-store' }); // Keep no-store too
+                if (!response.ok) {
+                    throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
+                }
+                const clusterState = await response.json();
+                const nodesData = transformClusterStateToTopology(clusterState);
+                renderNodes(nodesData);
+
+                // If a node was selected, and it still exists, refresh its details
+                if (currentlySelectedNodeId && nodesData[currentlySelectedNodeId]) {
+                    showNodeDetails(currentlySelectedNodeId, nodesData);
+                } else if (currentlySelectedNodeId && !nodesData[currentlySelectedNodeId]) {
+                    // If selected node is gone, close panel and clear selection
+                    nodeDetailPanel.classList.remove('visible');
+                    currentlySelectedNodeId = null;
+                }
+
+                lastUpdatedElement.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
+            } catch (error) {
+                console.error("Failed to fetch topology data:", error);
+                if (topologyGraphContainer) {
+                    topologyGraphContainer.innerHTML = ''; // Clear previous content
+                    const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+                    textEl.setAttribute('x', '50%');
+                    textEl.setAttribute('y', '50%');
+                    textEl.setAttribute('alignment-baseline', 'middle');
+                    textEl.setAttribute('text-anchor', 'middle');
+                    textEl.setAttribute('fill', '#FFD700'); // Yellow color
+                    textEl.setAttribute('font-size', '14');
+                    textEl.textContent = `Connect a Mac to start`;
+                    topologyGraphContainer.appendChild(textEl);
+                }
+                lastUpdatedElement.textContent = `Update failed: ${new Date().toLocaleTimeString()}`;
+                // If fetch fails, also consider hiding the panel if a node was selected
+                if (currentlySelectedNodeId) {
+                    nodeDetailPanel.classList.remove('visible');
+                    currentlySelectedNodeId = null;
+                }
+            } finally {
+                isFetching = false;
+            }
+        }
+
+        // Transform ClusterState -> nodesData compatible with existing render logic
+        function _extractIpFromMultiaddr(ma) {
+            if (!ma || typeof ma !== 'string') return null;
+            const parts = ma.split('/');
+            const ip4Idx = parts.indexOf('ip4');
+            const ip6Idx = parts.indexOf('ip6');
+            const idx = ip4Idx >= 0 ? ip4Idx : ip6Idx;
+            if (idx >= 0 && parts.length > idx + 1) {
+                return parts[idx + 1];
+            }
+            return null;
+        }
+
+        function transformClusterStateToTopology(clusterState) {
+            const result = {};
+            if (!clusterState) return result;
+
+            // Process nodes from topology or fallback to node_profiles directly
+            let nodesToProcess = {};
+            
+            if (clusterState.topology && clusterState.topology.nodes) {
+                // Use topology.nodes array
+                clusterState.topology.nodes.forEach(node => {
+                    if (node.node_id && node.node_profile) {
+                        nodesToProcess[node.node_id] = node.node_profile;
+                    }
+                });
+            } else if (clusterState.node_profiles) {
+                // Fallback to node_profiles directly
+                nodesToProcess = clusterState.node_profiles;
+            }
+
+            // Transform each node
+            for (const nodeId in nodesToProcess) {
+                const nodeProfile = nodesToProcess[nodeId];
+                if (!nodeProfile) continue;
+
+                // Extract memory information
+                let memBytesTotal = 0;
+                let memBytesUsed = 0;
+                
+                if (nodeProfile.memory) {
+                    memBytesTotal = nodeProfile.memory.ram_total || 0;
+                    const ramAvailable = nodeProfile.memory.ram_available || 0;
+                    memBytesUsed = Math.max(memBytesTotal - ramAvailable, 0);
+                }
+
+                // Extract model information
+                const modelId = nodeProfile.model_id || 'Unknown';
+                const chipId = nodeProfile.chip_id || '';
+                const friendlyName = nodeProfile.friendly_name || `${nodeId.substring(0, 8)}...`;
+
+                // Extract network addresses
+                const addrList = [];
+                if (nodeProfile.network_interfaces) {
+                    nodeProfile.network_interfaces.forEach(intf => {
+                        if (intf.ip_address && !intf.ip_address.startsWith('fe80::')) {
+                            // Filter out link-local IPv6 addresses
+                            addrList.push(intf.ip_address);
+                        }
+                    });
+                }
+
+                // Transform system metrics to macmon_info format for compatibility
+                const systemInfo = nodeProfile.system || {};
+                const macmonInfo = {
+                    memory: {
+                        ram_total: memBytesTotal,
+                        ram_usage: memBytesUsed,
+                        ram_available: nodeProfile.memory?.ram_available || 0,
+                        swap_total: nodeProfile.memory?.swap_total || 0,
+                        swap_usage: (nodeProfile.memory?.swap_total || 0) - (nodeProfile.memory?.swap_available || 0)
+                    },
+                    // Convert new format to old format
+                    gpu_usage: systemInfo.gpu_usage ? [0, systemInfo.gpu_usage] : [0, 0],
+                    temp: {
+                        cpu_temp_avg: systemInfo.temp || null,
+                        gpu_temp_avg: systemInfo.temp || null // Using same temp for both in new format
+                    },
+                    sys_power: systemInfo.sys_power || null,
+                    pcpu_usage: systemInfo.pcpu_usage ? [0, systemInfo.pcpu_usage] : [0, 0],
+                    ecpu_usage: systemInfo.ecpu_usage ? [0, systemInfo.ecpu_usage] : [0, 0],
+                    ane_power: systemInfo.ane_power || 0,
+                    flops_fp16: systemInfo.flops_fp16 || 0,
+                    timestamp: new Date().toISOString()
+                };
+
+                result[nodeId] = {
+                    mem: memBytesTotal,
+                    addrs: addrList,
+                    last_addr_update: Date.now() / 1000,
+                    system_info: { 
+                        model_id: modelId,
+                        chip_id: chipId
+                    },
+                    macmon_info: macmonInfo,
+                    last_macmon_update: Date.now() / 1000,
+                    friendly_name: friendlyName
+                };
+            }
+
+            return result;
+        }
+
+        // --- Conditional Data Handling ---
+        if (!USE_MOCK_DATA) {
+            // Initial fetch for live data
+            fetchDataAndRender();
+            // Periodic refresh for live data
+            fetchIntervalId = setInterval(fetchDataAndRender, REFRESH_INTERVAL);
+        } else {
+            // Use Mock Data
+            lastUpdatedElement.textContent = "Using Mock Data";
+            const mockData = {
+                "mockNodeId1_MacBookPro": {
+                    "mem": 34359738368, // e.g. 32GB
+                    "addrs": ["192.168.1.101", "100.64.0.1"],
+                    "last_addr_update": (Date.now() / 1000) - Math.random() * 60,
+                    "system_info": { "model_id": "MacBook Pro", "chip_id": "Apple M-Series Ultra", "memory": 32768 },
+                    "macmon_info": {
+                        'all_power': 0.0,
+                        'ane_power': 0.0,
+                        'cpu_power': 0.0,
+                        'ecpu_usage': [2048, 0.1], // [absolute, percentage 0-1]
+                        'gpu_power': 0.0,
+                        'gpu_ram_power': 0.0,
+                        'gpu_usage': [384, 0.05],  // [absolute, percentage 0-1]
+                        'memory': {
+                            'ram_total': 34359738368, // 32GB
+                            'ram_usage': 8589934592,  // 8GB
+                            'swap_total': 4294967296, // 4GB
+                            'swap_usage': 0
+                        },
+                        'pcpu_usage': [2048, 0.2], // [absolute, percentage 0-1]
+                        'ram_power': 0.0,
+                        'sys_power': 15.0, // Watts
+                        'temp': {
+                            'cpu_temp_avg': 45.0,
+                            'gpu_temp_avg': 42.0
+                        },
+                        'timestamp': new Date().toISOString()
+                    },
+                    "last_macmon_update": (Date.now() / 1000) - 2,
+                    "friendly_name": "Dev Alpha MBP"
+                },
+                "mockNodeId2_MacMini": {
+                    "mem": 17179869184, // e.g. 16GB
+                    "addrs": ["192.168.1.102"],
+                    "last_addr_update": (Date.now() / 1000) - Math.random() * 120,
+                    "system_info": { "model_id": "Mac Mini", "chip_id": "Apple M-Series Max", "memory": 16384 },
+                    "macmon_info": {
+                        'all_power': 0.0,
+                        'ane_power': 0.0,
+                        'cpu_power': 0.0,
+                        'ecpu_usage': [4096, 0.05],
+                        'gpu_power': 0.0,
+                        'gpu_ram_power': 0.0,
+                        'gpu_usage': [512, 0.02],
+                        'memory': {
+                            'ram_total': 17179869184,
+                            'ram_usage': 4294967296, // 4GB
+                            'swap_total': 0,
+                            'swap_usage': 0
+                        },
+                        'pcpu_usage': [4096, 0.1],
+                        'ram_power': 0.0,
+                        'sys_power': 25.0,
+                        'temp': {
+                            'cpu_temp_avg': 55.0,
+                            'gpu_temp_avg': 50.0
+                        },
+                        'timestamp': new Date().toISOString()
+                    },
+                    "last_macmon_update": (Date.now() / 1000) - 5,
+                    "friendly_name": "Build Server Mini"
+                },
+                "mockNodeId3_MacStudio": { // New Mac Studio Mock Node
+                    "mem": 137438953472, // 128GB
+                    "addrs": ["192.168.1.103"],
+                    "last_addr_update": (Date.now() / 1000) - Math.random() * 30,
+                    "system_info": { "model_id": "Mac Studio", "chip_id": "Apple M-Ultra Fusion Max Pro", "memory": 131072 },
+                    "macmon_info": {
+                        'all_power': 0.0,
+                        'ane_power': 0.0,
+                        'cpu_power': 0.0,
+                        'ecpu_usage': [8192, 0.02],
+                        'gpu_power': 0.0,
+                        'gpu_ram_power': 0.0,
+                        'gpu_usage': [1024, 0.01],
+                        'memory': {
+                            'ram_total': 137438953472,
+                            'ram_usage': 34359738368, // 32GB used
+                            'swap_total': 8589934592,
+                            'swap_usage': 0
+                        },
+                        'pcpu_usage': [8192, 0.05],
+                        'ram_power': 0.0,
+                        'sys_power': 45.0,
+                        'temp': {
+                            'cpu_temp_avg': 60.0,
+                            'gpu_temp_avg': 58.0
+                        },
+                        'timestamp': new Date().toISOString()
+                    },
+                    "last_macmon_update": (Date.now() / 1000) - 3,
+                    "friendly_name": "Graphics Studio Ultra"
+                }
+            };
+
+            function updateMockData() {
+                for (const nodeId in mockData) {
+                    const node = mockData[nodeId];
+                    node.last_addr_update = (Date.now() / 1000) - (Math.random() * 10);
+                    node.last_macmon_update = (Date.now() / 1000) - (Math.random() * 3 + 1); // More recent
+
+                    if (node.macmon_info) {
+                        const mi = node.macmon_info;
+                        mi.ecpu_usage[1] = Math.random() * 0.5; // Random % for E-cores
+                        mi.pcpu_usage[1] = Math.random() * 0.8; // Random % for P-cores
+                        mi.gpu_usage[1] = Math.random() * 0.9;  // Random % for GPU
+
+                        mi.memory.ram_usage = Math.random() * mi.memory.ram_total;
+                        if (mi.memory.swap_total > 0) {
+                            mi.memory.swap_usage = Math.random() * mi.memory.swap_total * 0.2; // Less swap usage
+                        }
+
+                        mi.ane_power = Math.random() > 0.6 ? Math.random() * 5 : 0; // Watts, sometimes active
+                        mi.cpu_power = (mi.pcpu_usage[1] * 15) + (mi.ecpu_usage[1] * 5) + (Math.random() * 5); // Approx Watts
+                        mi.gpu_power = (mi.gpu_usage[1] * 20) + (Math.random() * 3); // Approx Watts
+                        mi.ram_power = (mi.memory.ram_usage / mi.memory.ram_total) * 2 + (Math.random() * 0.5); // Approx Watts
+                        mi.all_power = mi.ane_power + mi.cpu_power + mi.gpu_power + mi.ram_power + 5; // Base + components
+                        mi.sys_power = mi.all_power + Math.random() * 2; // sys_power is usually a bit more than all_power sum
+
+                        mi.temp.cpu_temp_avg = 35 + (mi.pcpu_usage[1] + mi.ecpu_usage[1]) * 30 + (Math.random() * 10 - 5);
+                        mi.temp.gpu_temp_avg = 30 + mi.gpu_usage[1] * 50 + (Math.random() * 10 - 5);
+                        mi.timestamp = new Date().toISOString();
+                    }
+                }
+                renderNodes(mockData);
+                lastUpdatedElement.textContent = `Last updated: ${new Date().toLocaleTimeString()} (Mock Data)`;
+
+                if (currentlySelectedNodeId && mockData[currentlySelectedNodeId]) {
+                    showNodeDetails(currentlySelectedNodeId, mockData);
+                } else if (currentlySelectedNodeId && !mockData[currentlySelectedNodeId]) {
+                    nodeDetailPanel.classList.remove('visible');
+                    currentlySelectedNodeId = null;
+                }
+            }
+            updateMockData(); // Initial render with mock
+            setInterval(updateMockData, REFRESH_INTERVAL);
+        }
+
+
+        // Mock data for local testing if the API is not available
+        // Comment out fetchDataAndRender() and setInterval() above
+        // and uncomment the block below to use mock data.
+        /*  <<<< This comment and the one below should be removed or adjusted
+        const mockData = {
+            "12D3KooWEbiTv9MkyNu5aVi4A7A2xHhwyrFxPEqso2ciJtPDjKcn": {
+// ... existing old mock data ...
+        setInterval(updateMockData, REFRESH_INTERVAL); // Update mock data every second
+        */
+
+    </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/master/api.py b/master/api.py
index 69efb21b..ba74077f 100644
--- a/master/api.py
+++ b/master/api.py
@@ -1,4 +1,5 @@
 import asyncio
+from pathlib import Path
 import time
 from collections.abc import AsyncGenerator
 from typing import Callable, List, Sequence, final
@@ -7,6 +8,7 @@ import uvicorn
 from fastapi import FastAPI, HTTPException
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.responses import StreamingResponse
+from fastapi.staticfiles import StaticFiles
 
 from shared.db.sqlite.connector import AsyncSQLiteEventStorage
 from shared.models.model_cards import MODEL_CARDS
@@ -38,6 +40,10 @@ from shared.types.tasks import ChatCompletionTaskParams
 from shared.types.worker.common import InstanceId
 from shared.types.worker.instances import Instance
 
+# TODO: Make sure that when we package the app the dashboard is in the right place.
+_ROOT_DIR = Path(__file__).resolve().parents[1]
+_DASHBOARD_DIR = _ROOT_DIR / "dashboard"
+
 
 def chunk_to_response(chunk: TokenChunk) -> ChatCompletionResponse:
     return ChatCompletionResponse(
@@ -74,6 +80,8 @@ class API:
         self.global_events = global_events
         self.get_state = get_state
 
+        self._app.mount("/", StaticFiles(directory=_DASHBOARD_DIR, html=True), name="dashboard")
+
     def _setup_cors(self) -> None:
         self._app.add_middleware(
             CORSMiddleware,
@@ -84,35 +92,18 @@ class API:
         )
 
     def _setup_routes(self) -> None:
-        # self._app.get("/topology/control_plane")(self.get_control_plane_topology)
-        # self._app.get("/topology/data_plane")(self.get_data_plane_topology)
-        # self._app.get("/instances/list")(self.list_instances)
         self._app.post("/instance")(self.create_instance)
         self._app.get("/instance/{instance_id}")(self.get_instance)
         self._app.delete("/instance/{instance_id}")(self.delete_instance)
         self._app.get("/models")(self.get_models)
         self._app.get("/v1/models")(self.get_models)
-        # self._app.get("/model/{model_id}/metadata")(self.get_model_data)
-        # self._app.post("/model/{model_id}/instances")(self.get_instances_by_model)
         self._app.post("/v1/chat/completions")(self.chat_completions)
+        self._app.get("/state")(self.get_state)
 
     @property
     def app(self) -> FastAPI:
         return self._app
 
-    # def get_control_plane_topology(self):
-    #     return {"message": "Hello, World!"}
-
-    # def get_data_plane_topology(self):
-    #     return {"message": "Hello, World!"}
-
-    # def get_model_metadata(self, model_id: ModelId) -> ModelMetadata: ...
-
-    # def download_model(self, model_id: ModelId) -> None: ...
-
-    # def list_instances(self):
-    #     return {"message": "Hello, World!"}
-
     async def create_instance(self, payload: CreateInstanceTaskParams) -> CreateInstanceResponse:
         model_meta = await resolve_model_meta(payload.model_id)
 
@@ -153,10 +144,6 @@ class API:
             instance_id=instance_id,
         )
 
-    # def get_model_data(self, model_id: ModelId) -> ModelInfo: ...
-
-    # def get_instances_by_model(self, model_id: ModelId) -> list[Instance]: ...
-
     async def _generate_chat_stream(self, command_id: CommandId) -> AsyncGenerator[str, None]:
         """Generate chat completion stream as JSON strings."""
 
@@ -221,6 +208,8 @@ class API:
                 description=card.description,
                 tags=card.tags) for card in MODEL_CARDS.values()])
 
+    async def get_state(self) -> State:
+        return self.get_state()
 
 
 def start_fastapi_server(

← a2b4093d add metrics: gpu_usage, temp, sys_power, pcpu_usage, ecpu_us  ·  back to Exo  ·  Update CODEOWNERS 25fa46c6 →