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 (