v0.2.4 - Hotfix: Unknown status + copy UX + update banner context
Bug fix: - config.py: empty PARAKEET_CONTAINER / MAGPIE_CONTAINER env vars (from migrating to v0.2.0+ where the field is optional and saved as '') now fall back to 'parakeet-asr' / 'magpie-tts' via the 'or' idiom. Confirmed live: services classify as 'running' instead of 'unknown'. UX: - Replaced text 'Copy' buttons with compact icon buttons (clipboard SVG) - Endpoint Base URL + Model ID + curl snippet are now click-to-copy themselves (the value AND a separate icon button) - Service cards: host, base URL, and model are now three separate copyable rows - Update banner: leading explanatory line — 'Updates to eugr/spark-vllm-docker — the upstream project that orchestrates vLLM on your Sparks. These are not firmware, OS, or model updates.' with a link to the repo.
This commit is contained in:
+48
-23
@@ -143,11 +143,16 @@ async function renderServices() {
|
||||
if (action === 'stop' && cls !== 'running' && cls !== 'starting' && cls !== 'unhealthy') return true;
|
||||
return false;
|
||||
};
|
||||
const copyIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
|
||||
const hostStr = s.host ? `${s.host}:${s.port}` : '';
|
||||
const hostRow = s.host
|
||||
? `<div class="row"><span class="k">Host</span><span class="v">${escapeHtml(s.host)}:${s.port}</span></div>`
|
||||
? `<div class="row"><span class="k">Host</span><span class="v copyable" data-copy-self title="Click to copy">${escapeHtml(hostStr)}</span><button class="icon-btn" data-copy-text="${escapeHtml(hostStr)}" title="Copy host" aria-label="Copy">${copyIcon}</button></div>`
|
||||
: `<div class="row"><span class="k">Host</span><span class="v muted-v">not configured</span></div>`;
|
||||
const urlRow = s.base_url
|
||||
? `<div class="row"><span class="k">URL</span><span class="v copyable" data-copy-self title="Click to copy">${escapeHtml(s.base_url)}</span><button class="icon-btn" data-copy-text="${escapeHtml(s.base_url)}" title="Copy URL" aria-label="Copy">${copyIcon}</button></div>`
|
||||
: '';
|
||||
const modelRow = s.model
|
||||
? `<div class="row"><span class="k">Model</span><span class="v">${escapeHtml(s.model)}</span></div>`
|
||||
? `<div class="row"><span class="k">Model</span><span class="v copyable" data-copy-self title="Click to copy">${escapeHtml(s.model)}</span><button class="icon-btn" data-copy-text="${escapeHtml(s.model)}" title="Copy model" aria-label="Copy">${copyIcon}</button></div>`
|
||||
: '';
|
||||
const restartsRow = s.restart_count != null && s.restart_count > 1
|
||||
? `<div class="row"><span class="k">Restarts</span><span class="v">${s.restart_count}</span></div>`
|
||||
@@ -159,6 +164,7 @@ async function renderServices() {
|
||||
<span class="status">${statusLabel(cls)}</span>
|
||||
</div>
|
||||
${hostRow}
|
||||
${urlRow}
|
||||
${modelRow}
|
||||
${restartsRow}
|
||||
<div class="service-actions">
|
||||
@@ -212,31 +218,50 @@ function renderEndpoint(status) {
|
||||
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
|
||||
async function copyText(text, indicatorEl) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (indicatorEl) {
|
||||
indicatorEl.classList.add('copied');
|
||||
setTimeout(() => indicatorEl.classList.remove('copied'), 1200);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
// Plain HTTP fallback: select the text so the user can ⌘C
|
||||
if (indicatorEl) {
|
||||
const range = document.createRange();
|
||||
range.selectNode(target);
|
||||
range.selectNode(indicatorEl);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function setupCopyButtons() {
|
||||
document.body.addEventListener('click', async (e) => {
|
||||
// Inline icon copy with literal text (used for dynamically-rendered service rows)
|
||||
const litBtn = e.target.closest('[data-copy-text]');
|
||||
if (litBtn) {
|
||||
await copyText(litBtn.dataset.copyText, litBtn);
|
||||
return;
|
||||
}
|
||||
// Copy buttons (with svg icon) referenced by data-copy="selector"
|
||||
const btn = e.target.closest('[data-copy]');
|
||||
if (btn) {
|
||||
const target = el(btn.dataset.copy);
|
||||
if (target) {
|
||||
await copyText(target.textContent, btn);
|
||||
target.classList.add('copied');
|
||||
setTimeout(() => target.classList.remove('copied'), 1200);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Self-copy: clicking the text itself
|
||||
const selfCopy = e.target.closest('[data-copy-self]');
|
||||
if (selfCopy) {
|
||||
await copyText(selfCopy.textContent, selfCopy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user