diff --git a/image/app/health.py b/image/app/health.py index 16ab92a..698f623 100644 --- a/image/app/health.py +++ b/image/app/health.py @@ -7,16 +7,26 @@ _TIMEOUT = 3.0 async def check_vllm(settings: Settings) -> dict: + base_url = ( + f"http://{settings.spark1_host}:{settings.vllm_port}/v1" + if settings.spark1_host + else None + ) if not settings.spark1_host: - return {"ok": False, "error": "spark1 not configured"} + return {"ok": False, "error": "spark1 not configured", "base_url": base_url} try: async with httpx.AsyncClient(timeout=_TIMEOUT) as c: r = await c.get(f"http://{settings.spark1_host}:{settings.vllm_port}/v1/models") r.raise_for_status() ids = [m["id"] for m in r.json().get("data", [])] - return {"ok": True, "current_model": ids[0] if ids else None, "all": ids} + return { + "ok": True, + "current_model": ids[0] if ids else None, + "all": ids, + "base_url": base_url, + } except Exception as e: - return {"ok": False, "error": str(e)} + return {"ok": False, "error": str(e), "base_url": base_url} async def check_parakeet(settings: Settings) -> dict: diff --git a/image/app/static/app.js b/image/app/static/app.js index 2a2fa49..427c34e 100644 --- a/image/app/static/app.js +++ b/image/app/static/app.js @@ -83,6 +83,52 @@ function renderCurrent(status) { c.innerHTML = `${label}`; } +function renderEndpoint(status) { + const v = status.vllm || {}; + const panel = el('#endpoint-panel'); + const ready = v.ok && v.current_model && v.base_url; + panel.classList.toggle('hidden', !ready); + if (!ready) return; + el('#ep-url').textContent = v.base_url; + el('#ep-model').textContent = v.current_model; + const snippet = +`curl -s ${v.base_url}/chat/completions \\ + -H 'content-type: application/json' \\ + -d '{ + "model": "${v.current_model}", + "messages": [{"role": "user", "content": "Hello"}] + }'`; + el('#ep-curl-snippet').textContent = snippet; +} + +function setupCopyButtons() { + document.body.addEventListener('click', async (e) => { + const btn = e.target.closest('.copy-btn'); + if (!btn) return; + const targetSel = btn.dataset.copy; + if (!targetSel) return; + const target = el(targetSel); + if (!target) return; + const text = target.textContent; + try { + await navigator.clipboard.writeText(text); + const original = btn.textContent; + btn.classList.add('copied'); + btn.textContent = 'Copied'; + setTimeout(() => { + btn.classList.remove('copied'); + btn.textContent = original; + }, 1400); + } catch { + // Clipboard API may fail over plain HTTP; fall back to selection + const range = document.createRange(); + range.selectNode(target); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + } + }); +} + function renderHealth(status) { function setDot(id, ok, payload) { const item = el(id); @@ -221,6 +267,7 @@ async function pollStatus() { state.configured = status.configured; renderBanner(status); renderCurrent(status); + renderEndpoint(status); renderHealth(status); if (status.current_swap_job && status.current_swap_job !== state.swap_job_id) { attachToSwap(status.current_swap_job, /*needsBackfill=*/true); @@ -342,6 +389,7 @@ function appendLog(line) { } async function init() { + setupCopyButtons(); await loadModels(); await pollStatus(); setInterval(pollStatus, 5000); diff --git a/image/app/static/index.html b/image/app/static/index.html index 7710ee4..98c4190 100644 --- a/image/app/static/index.html +++ b/image/app/static/index.html @@ -24,6 +24,25 @@ Run the Configure Sparks action in StartOS to set hostnames, then run Test Connection. + +