diff --git a/emmc-provisioning/dashboard/app.py b/emmc-provisioning/dashboard/app.py index 8842d9b..9336d1f 100644 --- a/emmc-provisioning/dashboard/app.py +++ b/emmc-provisioning/dashboard/app.py @@ -1629,6 +1629,18 @@ def api_build_cloudinit_cancel(): return jsonify({"ok": False, "error": str(e)}), 500 +@app.route("/api/build-cloudinit-status-clear", methods=["POST"]) +@require_admin +def api_build_cloudinit_status_clear(): + """Clear build status to idle (so message is cleared after cancel/done/error).""" + st = _build_status_read() + busy = st.get("phase") in ("downloading", "decompressing", "injecting", "finalizing", "resolving") + if busy: + return jsonify({"ok": False, "error": "Cannot clear while build is in progress"}), 409 + _build_status_write("idle", "", None, None) + return jsonify({"ok": True}) + + @app.route("/api/build-cloudinit", methods=["POST"]) @require_admin def api_build_cloudinit(): diff --git a/emmc-provisioning/dashboard/templates/cloudinit_build.html b/emmc-provisioning/dashboard/templates/cloudinit_build.html index a0d8175..daff894 100644 --- a/emmc-provisioning/dashboard/templates/cloudinit_build.html +++ b/emmc-provisioning/dashboard/templates/cloudinit_build.html @@ -81,6 +81,7 @@
+
@@ -140,14 +141,27 @@ var el = document.getElementById('buildCloudInitStatus'); var btn = document.getElementById('buildCloudInitBtn'); var cancelBtn = document.getElementById('buildCloudInitCancelBtn'); + var dismissEl = document.getElementById('buildCloudInitDismiss'); var busy = ['resolving','downloading','decompressing','injecting','finalizing'].indexOf(d.phase) >= 0; if (btn) btn.disabled = busy; if (cancelBtn) { cancelBtn.style.display = busy ? 'inline-block' : 'none'; cancelBtn.disabled = false; } + if (dismissEl) dismissEl.style.display = (d.phase === 'done' || d.phase === 'error' || d.phase === 'cancelled') ? 'inline' : 'none'; if (d.phase === 'idle' && !d.message) el.textContent = ''; else if (d.phase === 'done') { el.textContent = 'Done: ' + (d.output_name || '') + ' — see Admin page Cloud-init images.'; } else if (d.phase === 'error') el.textContent = 'Error: ' + (d.error || ''); - else if (d.phase === 'cancelled') el.textContent = 'Build cancelled.'; - else el.textContent = (d.phase || '') + ': ' + (d.message || ''); + else if (d.phase === 'cancelled') { + el.textContent = 'Build cancelled.'; + if (btn) btn.disabled = false; + if (!window._buildClearScheduled) { + window._buildClearScheduled = true; + setTimeout(function() { + authFetch('/api/build-cloudinit-status-clear', { method: 'POST', headers: {'Content-Type':'application/json'} }).then(function() { fetchBuildStatus(); }).finally(function() { window._buildClearScheduled = false; }); + }, 3000); + } + } else { + window._buildClearScheduled = false; + el.textContent = (d.phase || '') + ': ' + (d.message || ''); + } if (busy) setTimeout(fetchBuildStatus, 5000); }).catch(function() {}); } @@ -228,6 +242,8 @@ document.getElementById('buildCloudInitBtn').onclick = startBuild; var cancelBuildBtn = document.getElementById('buildCloudInitCancelBtn'); if (cancelBuildBtn) cancelBuildBtn.onclick = cancelBuild; + var dismissBuildBtn = document.getElementById('buildCloudInitDismiss'); + if (dismissBuildBtn) dismissBuildBtn.onclick = function(e) { e.preventDefault(); authFetch('/api/build-cloudinit-status-clear', { method: 'POST', headers: {'Content-Type':'application/json'} }).then(function() { fetchBuildStatus(); }); }; document.getElementById('buildVariant').onchange = function() { authFetch('/api/raspios-latest-url?variant=' + encodeURIComponent(document.getElementById('buildVariant').value)).then(function(r) { return r.json(); }).then(function(d) { document.getElementById('buildRaspiosUrl').textContent = (d.ok && d.filename) ? d.filename : (d.error || ''); diff --git a/emmc-provisioning/dashboard/templates/index.html b/emmc-provisioning/dashboard/templates/index.html index a2f6eea..6c598b0 100644 --- a/emmc-provisioning/dashboard/templates/index.html +++ b/emmc-provisioning/dashboard/templates/index.html @@ -451,6 +451,7 @@
+
Cloud-init templates & customize
@@ -895,10 +896,12 @@ var el = document.getElementById('buildCloudInitStatus'); var btn = document.getElementById('buildCloudInitBtn'); var cancelBtn = document.getElementById('buildCloudInitCancelBtn'); + var dismissEl = document.getElementById('buildCloudInitDismiss'); if (!el) return; var busy = ['resolving','downloading','decompressing','injecting','finalizing'].indexOf(d.phase) >= 0; if (btn) btn.disabled = busy; if (cancelBtn) { cancelBtn.style.display = busy ? 'inline-block' : 'none'; cancelBtn.disabled = false; } + if (dismissEl) dismissEl.style.display = (d.phase === 'done' || d.phase === 'error' || d.phase === 'cancelled') ? 'inline' : 'none'; if (d.phase === 'idle' && !d.message) { el.textContent = ''; } else if (d.phase === 'done') { @@ -909,7 +912,15 @@ el.textContent = 'Error: ' + (d.error || d.message || 'Unknown'); } else if (d.phase === 'cancelled') { el.textContent = 'Build cancelled.'; + if (btn) btn.disabled = false; + if (!window._buildClearScheduled) { + window._buildClearScheduled = true; + setTimeout(function() { + fetch('/api/build-cloudinit-status-clear', { method: 'POST', headers: { 'Content-Type': 'application/json' } }).then(function() { fetchBuildStatus(); }).finally(function() { window._buildClearScheduled = false; }); + }, 3000); + } } else { + window._buildClearScheduled = false; el.textContent = (d.phase || '') + ': ' + (d.message || ''); } if (busy) setTimeout(fetchBuildStatus, 5000); @@ -1012,6 +1023,8 @@ if (buildBtn) buildBtn.onclick = startBuildCloudInit; var cancelBuildBtn = document.getElementById('buildCloudInitCancelBtn'); if (cancelBuildBtn) cancelBuildBtn.onclick = cancelBuildCloudInit; + var dismissBuildBtn = document.getElementById('buildCloudInitDismiss'); + if (dismissBuildBtn) dismissBuildBtn.onclick = function(e) { e.preventDefault(); fetch('/api/build-cloudinit-status-clear', { method: 'POST', headers: { 'Content-Type': 'application/json' } }).then(function() { fetchBuildStatus(); }); }; var variantSel = document.getElementById('buildVariant'); if (variantSel) variantSel.onchange = fetchRaspiosUrl; var templateLoadBtn = document.getElementById('buildTemplateLoad'); diff --git a/emmc-provisioning/host/build-cloudinit-image.sh b/emmc-provisioning/host/build-cloudinit-image.sh index 2f73937..414999e 100644 --- a/emmc-provisioning/host/build-cloudinit-image.sh +++ b/emmc-provisioning/host/build-cloudinit-image.sh @@ -190,11 +190,24 @@ mkdir -p "$OUTPUT_DIR" cp "$IMG_FILE" "$OUT_PATH" # Compress to .img.xz to reduce size (deploy supports decompress on the fly) +check_cancel write_status "finalizing" "Compressing image (xz)…" "" "" OUT_XZ="${OUT_PATH}.xz" -# -T 0 = use all cores; fallback to -T 1 if old xz -if ! xz -T 0 -z -k -f "$OUT_PATH" 2>/dev/null; then - xz -T 1 -z -k -f "$OUT_PATH" 2>/dev/null || true +xz -T 0 -z -k -f "$OUT_PATH" 2>/dev/null & +XZ_PID=$! +while kill -0 "$XZ_PID" 2>/dev/null; do + if [[ -f "$CANCEL_FILE" ]]; then + kill "$XZ_PID" 2>/dev/null + wait "$XZ_PID" 2>/dev/null + write_status "cancelled" "Build cancelled." "" "" + rm -f "$REQUEST_FILE" "$CANCEL_FILE" + exit 0 + fi + sleep 2 +done +wait "$XZ_PID" 2>/dev/null || true +if [[ ! -f "$OUT_XZ" ]]; then + xz -T 1 -z -k -f "$OUT_PATH" 2>/dev/null || true fi if [[ -f "$OUT_XZ" ]]; then rm -f "$OUT_PATH"