From 221ff0f28f40c116dce6387007e031344247fe2d Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 31 Mar 2026 15:37:58 +0000 Subject: [PATCH] feat(ui): show cluster status in home in distributed mode Signed-off-by: Ettore Di Giacinto --- core/http/react-ui/src/App.css | 17 +++++- core/http/react-ui/src/pages/Home.jsx | 81 ++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/core/http/react-ui/src/App.css b/core/http/react-ui/src/App.css index d69a241a7..7e741d73a 100644 --- a/core/http/react-ui/src/App.css +++ b/core/http/react-ui/src/App.css @@ -3421,7 +3421,7 @@ /* Home resource bar - prominent */ .home-resource-bar { width: 100%; - max-width: 320px; + max-width: 380px; padding: var(--spacing-sm) var(--spacing-md); background: var(--color-bg-secondary); border: 1px solid var(--color-border-subtle); @@ -3456,6 +3456,21 @@ border-radius: 3px; transition: width 500ms ease; } +.home-cluster-status { + display: flex; + align-items: center; + gap: var(--spacing-xs); + font-size: 0.75rem; + color: var(--color-text-muted); + margin-top: var(--spacing-xs); +} +.home-cluster-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-success); + display: inline-block; +} /* Home chat card */ .home-chat-card { diff --git a/core/http/react-ui/src/pages/Home.jsx b/core/http/react-ui/src/pages/Home.jsx index 2b40cb54b..54a87b589 100644 --- a/core/http/react-ui/src/pages/Home.jsx +++ b/core/http/react-ui/src/pages/Home.jsx @@ -7,9 +7,15 @@ import { CAP_CHAT } from '../utils/capabilities' import UnifiedMCPDropdown from '../components/UnifiedMCPDropdown' import ConfirmDialog from '../components/ConfirmDialog' import { useResources } from '../hooks/useResources' -import { fileToBase64, backendControlApi, systemApi, modelsApi, mcpApi } from '../utils/api' +import { fileToBase64, backendControlApi, systemApi, modelsApi, mcpApi, nodesApi } from '../utils/api' import { API_CONFIG } from '../utils/config' +function formatBytes(bytes) { + if (!bytes || bytes === 0) return null + const gb = bytes / (1024 * 1024 * 1024) + return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / (1024 * 1024)).toFixed(0)} MB` +} + export default function Home() { const navigate = useNavigate() const { addToast } = useOutletContext() @@ -32,10 +38,55 @@ export default function Home() { const [mcpSelectedServers, setMcpSelectedServers] = useState([]) const [clientMCPSelectedIds, setClientMCPSelectedIds] = useState([]) const [confirmDialog, setConfirmDialog] = useState(null) + const [distributedMode, setDistributedMode] = useState(false) + const [clusterData, setClusterData] = useState(null) const imageInputRef = useRef(null) const audioInputRef = useRef(null) const fileInputRef = useRef(null) + // Detect distributed mode + useEffect(() => { + fetch(apiUrl('/api/features')) + .then(r => r.json()) + .then(data => setDistributedMode(!!data.distributed)) + .catch(() => {}) + }, []) + + // Poll cluster node data in distributed mode + useEffect(() => { + if (!distributedMode) return + const fetchCluster = async () => { + try { + const data = await nodesApi.list() + const nodes = Array.isArray(data) ? data : [] + const backendNodes = nodes.filter(n => !n.node_type || n.node_type === 'backend') + const totalVRAM = backendNodes.reduce((sum, n) => sum + (n.total_vram || 0), 0) + const usedVRAM = backendNodes.reduce((sum, n) => { + if (n.total_vram && n.available_vram != null) return sum + (n.total_vram - n.available_vram) + return sum + }, 0) + const totalRAM = backendNodes.reduce((sum, n) => sum + (n.total_ram || 0), 0) + const usedRAM = backendNodes.reduce((sum, n) => { + if (n.total_ram && n.available_ram != null) return sum + (n.total_ram - n.available_ram) + return sum + }, 0) + const isGPU = totalVRAM > 0 + const healthyCount = backendNodes.filter(n => n.status === 'healthy').length + const totalCount = backendNodes.length + setClusterData({ + totalMem: isGPU ? totalVRAM : totalRAM, + usedMem: isGPU ? usedVRAM : usedRAM, + isGPU, + healthyCount, + totalCount, + }) + } catch { setClusterData(null) } + } + fetchCluster() + const interval = setInterval(fetchCluster, 5000) + return () => clearInterval(interval) + }, [distributedMode]) + // Fetch configured models (to know if any exist) and loaded models (currently running) const fetchSystemInfo = useCallback(async () => { try { @@ -205,6 +256,10 @@ export default function Home() { const usagePct = resources?.aggregate?.usage_percent ?? resources?.ram?.usage_percent ?? 0 const pctColor = usagePct > 90 ? 'var(--color-error)' : usagePct > 70 ? 'var(--color-warning)' : 'var(--color-success)' + // Cluster resource display (distributed mode) + const clusterUsagePct = clusterData?.totalMem > 0 ? ((clusterData.usedMem / clusterData.totalMem) * 100) : 0 + const clusterPctColor = clusterUsagePct > 90 ? 'var(--color-error)' : clusterUsagePct > 70 ? 'var(--color-warning)' : 'var(--color-success)' + return (
{hasModels ? ( @@ -215,7 +270,27 @@ export default function Home() {
{/* Resource monitor - prominent placement */} - {resources && ( + {distributedMode && clusterData && clusterData.totalMem > 0 ? ( +
+
+ + Cluster {clusterData.isGPU ? 'VRAM' : 'RAM'} + + {formatBytes(clusterData.usedMem)} / {formatBytes(clusterData.totalMem)} + +
+
+
+
+
+ + {clusterData.healthyCount}/{clusterData.totalCount} nodes online +
+
+ ) : !distributedMode && resources ? (
@@ -231,7 +306,7 @@ export default function Home() { />
- )} + ) : null} {/* Chat input form */}