From 1bbe743dba3282767c22f637afc355a348046a50 Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 28 Mar 2026 20:42:46 -0400 Subject: [PATCH] fix: capture job baseline before POST to avoid race condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version snapshotted last_run_id after the 201 response, but jobs fire immediately server-side — by the time the client fetched /api/jobs the runs were already complete, so the baseline matched the new state and the poll loop never detected completion. Baseline is now captured before the creation POST so it always reflects pre-run state regardless of job speed. Co-Authored-By: Claude Sonnet 4.6 --- js/ui.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/js/ui.js b/js/ui.js index b8e42a0..b691475 100644 --- a/js/ui.js +++ b/js/ui.js @@ -289,6 +289,10 @@ async function saveInstance() { hardware_acceleration: +document.getElementById('f-hardware-accel').checked, }; + // Snapshot job state before creation — jobs fire immediately after the 201 + // so the baseline must be captured before the POST, not after. + const jobBaseline = !editingVmid ? await _snapshotJobBaseline() : null; + const result = editingVmid ? await updateInstance(editingVmid, data) : await createInstance(data); @@ -298,7 +302,7 @@ async function saveInstance() { showToast(`${name} ${editingVmid ? 'updated' : 'created'}`, 'success'); closeModal(); - if (!editingVmid) await _waitForOnCreateJobs(); + if (jobBaseline) await _waitForOnCreateJobs(jobBaseline); if (currentVmid && document.getElementById('page-detail').classList.contains('active')) { await renderDetailPage(vmid); @@ -307,16 +311,18 @@ async function saveInstance() { } } -async function _waitForOnCreateJobs() { +async function _snapshotJobBaseline() { + const jobs = await fetch('/api/jobs').then(r => r.json()); + return new Map(jobs.map(j => [j.id, j.last_run_id ?? null])); +} + +async function _waitForOnCreateJobs(baseline) { const jobs = await fetch('/api/jobs').then(r => r.json()); const relevant = jobs.filter(j => { try { return JSON.parse(j.config || '{}').run_on_create; } catch { return false; } }); if (!relevant.length) return; - // Snapshot run IDs before jobs fire so we can detect new completions - const baseline = new Map(relevant.map(j => [j.id, j.last_run_id ?? null])); - const deadline = Date.now() + 30_000; while (Date.now() < deadline) { await new Promise(r => setTimeout(r, 500));