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 @@
+ Dismiss
@@ -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 @@
+ Dismiss
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"