v0.2.2 - spark-vllm-docker update checks + Apply Update
Backend:
- updates.py: get_update_status() runs git fetch + git rev-list --left-right --count HEAD...origin/main to learn ahead/behind/dirty, plus git log for pending commits
- UpdateManager class with asyncio.Lock; one update at a time
- POST /api/updates/apply triggers "git pull --ff-only && ./build-and-copy.sh -c" over SSH with streamed log + phase detection (Pulling / Building the vLLM container / Copying to peer Sparks)
- GET /api/updates returns {ok, behind, ahead, dirty, current, log[], branch}
Frontend:
- Persistent banner near footer: hidden when up-to-date, blue when N commits behind, warn (orange) when local dirty changes block update
- 'Show details' expands a list of pending commits
- 'Apply update' triggers the long-running build with phase + elapsed timer + collapsible logs
- Confirmation dialog explains the 5–40 min duration
Package: bump 0.2.2:0
This commit is contained in:
@@ -629,16 +629,152 @@ function handleDownloadDone(d) {
|
||||
dlState.job_id = null;
|
||||
}
|
||||
|
||||
// ===================== updates (spark-vllm-docker) =====================
|
||||
|
||||
const updState = {
|
||||
info: null,
|
||||
job_id: null,
|
||||
eventsource: null,
|
||||
started_at: null,
|
||||
timer_handle: null,
|
||||
};
|
||||
|
||||
async function pollUpdates() {
|
||||
try {
|
||||
const info = await fetchJSON('/api/updates');
|
||||
updState.info = info;
|
||||
renderUpdateBanner();
|
||||
} catch (e) {
|
||||
console.warn('updates poll failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderUpdateBanner() {
|
||||
const banner = el('#update-banner');
|
||||
const info = updState.info;
|
||||
const text = el('#ub-text');
|
||||
const details = el('#ub-details');
|
||||
const apply = el('#ub-apply');
|
||||
const list = el('#ub-list');
|
||||
const log = el('#ub-log');
|
||||
|
||||
if (!info || !info.ok) {
|
||||
banner.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
banner.classList.remove('hidden');
|
||||
const behind = info.behind || 0;
|
||||
const dirty = info.dirty || 0;
|
||||
banner.classList.toggle('up-to-date', behind === 0 && !dirty);
|
||||
banner.classList.toggle('warn', !!dirty);
|
||||
|
||||
if (dirty > 0) {
|
||||
text.textContent = `${dirty} local change${dirty === 1 ? '' : 's'} in ~/spark-vllm-docker. Resolve before updating.`;
|
||||
details.classList.add('hidden');
|
||||
apply.classList.add('hidden');
|
||||
} else if (behind === 0) {
|
||||
text.textContent = `spark-vllm-docker is up to date (${info.current || ''})`;
|
||||
details.classList.add('hidden');
|
||||
apply.classList.add('hidden');
|
||||
list.classList.add('hidden');
|
||||
} else {
|
||||
text.textContent = `${behind} commit${behind === 1 ? '' : 's'} behind upstream`;
|
||||
details.classList.remove('hidden');
|
||||
apply.classList.remove('hidden');
|
||||
log.textContent = (info.log || []).join('\n') || '(no log)';
|
||||
}
|
||||
}
|
||||
|
||||
function ubTimerStart(startedAt) {
|
||||
updState.started_at = startedAt;
|
||||
if (updState.timer_handle) clearInterval(updState.timer_handle);
|
||||
const tick = () => {
|
||||
if (!updState.started_at) return;
|
||||
const sec = Math.max(0, Math.floor((Date.now() - updState.started_at) / 1000));
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = sec % 60;
|
||||
el('#ub-elapsed').textContent = `${m}:${s.toString().padStart(2, '0')}`;
|
||||
};
|
||||
tick();
|
||||
updState.timer_handle = setInterval(tick, 500);
|
||||
}
|
||||
|
||||
async function applyUpdate() {
|
||||
if (!confirm('This pulls the latest spark-vllm-docker and rebuilds the vLLM container. Can take 5–40 minutes; the cluster is unaffected until you swap to a different model. Continue?')) return;
|
||||
try {
|
||||
const r = await fetchJSON('/api/updates/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ mode: 'cluster' }),
|
||||
});
|
||||
attachToUpdate(r.job_id);
|
||||
} catch (e) {
|
||||
alert('Failed to start update: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function attachToUpdate(jobId) {
|
||||
updState.job_id = jobId;
|
||||
el('#ub-progress').classList.remove('hidden');
|
||||
el('#ub-apply').classList.add('hidden');
|
||||
el('#ub-stream').textContent = '';
|
||||
el('#ub-phase').textContent = 'Starting…';
|
||||
try {
|
||||
const snap = await fetchJSON(`/api/updates/${jobId}`);
|
||||
ubTimerStart(Date.parse(snap.started_at));
|
||||
el('#ub-phase').textContent = snap.phase || 'Working…';
|
||||
el('#ub-stream').textContent = (snap.lines || []).join('\n');
|
||||
if (snap.returncode !== null) { handleUpdateDone(snap); return; }
|
||||
} catch (e) {
|
||||
ubTimerStart(Date.now());
|
||||
}
|
||||
const es = new EventSource(`/api/updates/${jobId}/stream`);
|
||||
updState.eventsource = es;
|
||||
es.onmessage = (ev) => {
|
||||
try {
|
||||
const d = JSON.parse(ev.data);
|
||||
if (d.line !== undefined) {
|
||||
const log = el('#ub-stream');
|
||||
log.textContent += d.line + '\n';
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
es.addEventListener('phase', (ev) => {
|
||||
try { el('#ub-phase').textContent = JSON.parse(ev.data).phase; } catch {}
|
||||
});
|
||||
es.addEventListener('done', (ev) => {
|
||||
let d = {}; try { d = JSON.parse(ev.data); } catch {}
|
||||
handleUpdateDone(d);
|
||||
});
|
||||
es.onerror = () => { es.close(); updState.eventsource = null; };
|
||||
}
|
||||
|
||||
function handleUpdateDone(d) {
|
||||
if (updState.eventsource) { updState.eventsource.close(); updState.eventsource = null; }
|
||||
if (updState.timer_handle) { clearInterval(updState.timer_handle); updState.timer_handle = null; }
|
||||
el('#ub-phase').textContent = d.state === 'failed' ? `Failed (rc=${d.returncode})` : 'Done ✓ — re-check from the banner.';
|
||||
setTimeout(pollUpdates, 2000);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
setupCopyButtons();
|
||||
el('#open-download').addEventListener('click', openDownloadForm);
|
||||
el('#dl-cancel').addEventListener('click', closeDownloadPanel);
|
||||
el('#dl-start').addEventListener('click', startDownload);
|
||||
el('#dl-repo').addEventListener('keydown', (e) => { if (e.key === 'Enter') startDownload(); });
|
||||
el('#ub-details').addEventListener('click', () => {
|
||||
const list = el('#ub-list');
|
||||
list.classList.toggle('hidden');
|
||||
list.open = !list.open;
|
||||
});
|
||||
el('#ub-apply').addEventListener('click', applyUpdate);
|
||||
await loadModels();
|
||||
await pollStatus();
|
||||
await renderServices();
|
||||
pollUpdates();
|
||||
setInterval(pollStatus, 5000);
|
||||
setInterval(pollUpdates, 300000); // every 5 min
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user