// ModelGrid UI — vanilla client. Bundled into ts_bundled/bundle.ts for // the single-binary build, or served from disk in dev mode. const VIEWS = [ 'overview', 'cluster', 'gpus', 'deployments', 'models', 'access', 'logs', 'metrics', 'settings', ]; const view = document.getElementById('view'); const nodeIdent = document.getElementById('node-ident'); const nodeVersion = document.getElementById('node-version'); function parseHash() { const raw = location.hash.replace(/^#\/?/, ''); const [top = 'overview'] = raw.split('/').filter(Boolean); return VIEWS.includes(top) ? top : 'overview'; } function setActive(current) { document.querySelectorAll('.nav-items a').forEach((el) => { el.classList.toggle('active', el.dataset.view === current); }); } async function fetchHealth() { const res = await fetch('/_ui/overview', { headers: { accept: 'application/json' } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } function statusDot(status) { const ok = status === 'ok'; const warn = status === 'degraded'; const cls = ok ? 'ok' : warn ? 'warn' : 'err'; return ``; } async function renderOverview() { view.innerHTML = `

Overview

Loading…
`; try { const data = await fetchHealth(); const health = data.health; const containers = health.containers || 0; const models = health.models || 0; const gpus = health.gpus || 0; const uptime = health.uptime || 0; const detailEntries = Object.entries(health.details?.containers || {}); const runningContainers = detailEntries.filter(([, v]) => v === 'healthy').length; view.innerHTML = `

Overview

Fleet
${statusDot(health.status)}${health.status}
v${health.version} · up ${formatUptime(uptime)}
Deployments
${runningContainers} / ${containers}
${containers === 0 ? 'no deployments' : `${runningContainers} healthy`}
GPUs
${gpus}
${gpus === 0 ? 'no GPU detected' : 'detected'}
Models
${models}
served via OpenAI API

Deployments

${renderContainerTable(detailEntries)} `; if (data.node) { nodeIdent.textContent = `${data.node.name} · ${data.node.role}`; nodeVersion.textContent = `v${data.node.version}`; } } catch (err) { view.innerHTML = `

Overview

Failed to load: ${escapeHtml(String(err.message || err))}
`; } } function renderContainerTable(entries) { if (entries.length === 0) { return `
No deployments configured. Add one with modelgrid run <model>.
`; } const rows = entries.map(([id, state]) => ` ${escapeHtml(id)} ${statusDot(state === 'healthy' ? 'ok' : 'err')}${escapeHtml(state)} `).join(''); return `${rows}
ContainerHealth
`; } function renderPlaceholder(name) { view.innerHTML = `

${name}

This view is part of the UI concept (see readme.ui.md) but is not implemented yet. Use the CLI for now: modelgrid ${cliHint(name)}.
`; } function cliHint(view) { const map = { Cluster: 'cluster status', GPUs: 'gpu list', Deployments: 'ps', Models: 'model list', Access: 'config apikey list', Logs: 'service logs', Metrics: 'service status', Settings: 'config show', }; return map[view] || '--help'; } function formatUptime(s) { if (s < 60) return `${s}s`; if (s < 3600) return `${Math.floor(s / 60)}m`; if (s < 86400) return `${Math.floor(s / 3600)}h`; return `${Math.floor(s / 86400)}d`; } function escapeHtml(s) { return s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); } function route() { const current = parseHash(); setActive(current); switch (current) { case 'overview': return renderOverview(); case 'cluster': return renderPlaceholder('Cluster'); case 'gpus': return renderPlaceholder('GPUs'); case 'deployments': return renderPlaceholder('Deployments'); case 'models': return renderPlaceholder('Models'); case 'access': return renderPlaceholder('Access'); case 'logs': return renderPlaceholder('Logs'); case 'metrics': return renderPlaceholder('Metrics'); case 'settings': return renderPlaceholder('Settings'); } } window.addEventListener('hashchange', route); if (!location.hash) location.hash = '#/overview'; route();