diff --git a/pikit-api.py b/pikit-api.py index 2fb4091..0cb161f 100644 --- a/pikit-api.py +++ b/pikit-api.py @@ -5,6 +5,7 @@ import re import urllib.request import hashlib import fcntl +from functools import partial HOST = "127.0.0.1" PORT = 4000 @@ -598,6 +599,7 @@ def apply_update_stub(): manifest = None state["in_progress"] = True + state["status"] = "in_progress" state["progress"] = "Starting update…" save_update_state(state) @@ -642,7 +644,8 @@ def apply_update_stub(): ensure_dir(backup_dir) # Backup web and api if WEB_ROOT.exists(): - shutil.copytree(WEB_ROOT, backup_dir / "pikit-web") + ensure_dir(backup_dir / "pikit-web") + shutil.copytree(WEB_ROOT, backup_dir / "pikit-web", dirs_exist_ok=True) if API_PATH.exists(): shutil.copy2(API_PATH, backup_dir / "pikit-api.py") @@ -672,7 +675,7 @@ def apply_update_stub(): state["progress"] = None save_update_state(state) except urllib.error.HTTPError as e: - state["status"] = "up_to_date" + state["status"] = "error" state["message"] = f"No release available ({e.code})" except Exception as e: state["status"] = "error" @@ -714,17 +717,24 @@ def rollback_update_stub(): state["message"] = "Another update is running" save_update_state(state) return state + state["in_progress"] = True + state["status"] = "in_progress" + state["progress"] = "Rolling back…" + save_update_state(state) backups = sorted(BACKUP_ROOT.glob("*"), reverse=True) if not backups: state["status"] = "error" state["message"] = "No backup available to rollback." + state["in_progress"] = False + state["progress"] = None save_update_state(state) + release_lock(lock) return state target = backups[0] try: if (target / "pikit-web").exists(): shutil.rmtree(WEB_ROOT, ignore_errors=True) - shutil.copytree(target / "pikit-web", WEB_ROOT) + shutil.copytree(target / "pikit-web", WEB_ROOT, dirs_exist_ok=True) if (target / "pikit-api.py").exists(): shutil.copy2(target / "pikit-api.py", API_PATH) os.chmod(API_PATH, 0o755) @@ -735,9 +745,10 @@ def rollback_update_stub(): except Exception as e: state["status"] = "error" state["message"] = f"Rollback failed: {e}" + state["in_progress"] = False + state["progress"] = None save_update_state(state) - if lock: - release_lock(lock) + release_lock(lock) return state diff --git a/pikit-web/assets/main.js b/pikit-web/assets/main.js index e7c55e7..d641a69 100644 --- a/pikit-web/assets/main.js +++ b/pikit-web/assets/main.js @@ -106,6 +106,7 @@ const busyOverlay = document.getElementById("busyOverlay"); const busyTitle = document.getElementById("busyTitle"); const busyText = document.getElementById("busyText"); const toastContainer = document.getElementById("toastContainer"); +const readyOverlay = document.getElementById("readyOverlay"); const TOAST_POS_KEY = "pikit-toast-pos"; const TOAST_ANIM_KEY = "pikit-toast-anim"; @@ -398,6 +399,7 @@ async function loadReleaseStatus() { auto_check = false, progress = null, } = data || {}; + window.__lastReleaseState = data; setReleaseChip(data); if (releaseCurrent) releaseCurrent.textContent = current_version; if (releaseLatest) releaseLatest.textContent = latest_version; @@ -485,9 +487,10 @@ function wireReleaseControls() { releaseApplyBtn?.addEventListener("click", async () => { try { + showBusy("Updating Pi-Kit…", "Applying release. This can take up to a minute."); if (releaseProgress) releaseProgress.textContent = "Downloading and installing…"; await applyRelease(); - await loadReleaseStatus(); + pollReleaseStatus(); showToast("Update started", "success"); } catch (e) { showToast(e.error || "Update failed", "error"); @@ -498,10 +501,11 @@ function wireReleaseControls() { releaseRollbackBtn?.addEventListener("click", async () => { try { + showBusy("Rolling back…", "Restoring previous backup."); if (releaseProgress) releaseProgress.textContent = "Rolling back…"; await rollbackRelease(); - await loadReleaseStatus(); - showToast("Rollback complete", "success"); + pollReleaseStatus(); + showToast("Rollback started", "success"); } catch (e) { showToast(e.error || "Rollback failed", "error"); } finally { @@ -520,6 +524,27 @@ function wireReleaseControls() { }); } +function pollReleaseStatus() { + let attempts = 0; + const maxAttempts = 30; // ~1 min at 2s + const tick = async () => { + attempts += 1; + await loadReleaseStatus(); + const state = window.__lastReleaseState || {}; + if (state.status === "in_progress" && attempts < maxAttempts) { + setTimeout(tick, 2000); + } else { + hideBusy(); + if (state.status === "up_to_date") { + showToast("Update complete", "success"); + } else if (state.status === "error") { + showToast(state.message || "Update failed", "error"); + } + } + }; + tick(); +} + function showBusy(title = "Working…", text = "This may take a few seconds.") { if (!busyOverlay) return; busyTitle.textContent = title; diff --git a/pikit-web/assets/style.css b/pikit-web/assets/style.css index 57fd612..0cb8234 100644 --- a/pikit-web/assets/style.css +++ b/pikit-web/assets/style.css @@ -1374,6 +1374,11 @@ select:focus-visible, #addServiceModal .controls { padding: 0 2px 4px; } + +/* Busy overlay already defined; ensure modal width for release modal */ +#releaseModal .modal-card.wide { + max-width: 760px; +} .modal:not(.hidden) .modal-card { transform: translateY(0) scale(1); }