fix: capture job baseline before POST to avoid race condition
All checks were successful
CI / test (pull_request) Successful in 14s
CI / build-dev (pull_request) Has been skipped

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 20:42:46 -04:00
parent ddd528a682
commit 1bbe743dba

View File

@@ -289,6 +289,10 @@ async function saveInstance() {
hardware_acceleration: +document.getElementById('f-hardware-accel').checked, 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 const result = editingVmid
? await updateInstance(editingVmid, data) ? await updateInstance(editingVmid, data)
: await createInstance(data); : await createInstance(data);
@@ -298,7 +302,7 @@ async function saveInstance() {
showToast(`${name} ${editingVmid ? 'updated' : 'created'}`, 'success'); showToast(`${name} ${editingVmid ? 'updated' : 'created'}`, 'success');
closeModal(); closeModal();
if (!editingVmid) await _waitForOnCreateJobs(); if (jobBaseline) await _waitForOnCreateJobs(jobBaseline);
if (currentVmid && document.getElementById('page-detail').classList.contains('active')) { if (currentVmid && document.getElementById('page-detail').classList.contains('active')) {
await renderDetailPage(vmid); 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 jobs = await fetch('/api/jobs').then(r => r.json());
const relevant = jobs.filter(j => { const relevant = jobs.filter(j => {
try { return JSON.parse(j.config || '{}').run_on_create; } catch { return false; } try { return JSON.parse(j.config || '{}').run_on_create; } catch { return false; }
}); });
if (!relevant.length) return; 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; const deadline = Date.now() + 30_000;
while (Date.now() < deadline) { while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, 500)); await new Promise(r => setTimeout(r, 500));