Add update progress polling overlay and strengthen state flags
This commit is contained in:
21
pikit-api.py
21
pikit-api.py
@@ -5,6 +5,7 @@ import re
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import hashlib
|
import hashlib
|
||||||
import fcntl
|
import fcntl
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
HOST = "127.0.0.1"
|
HOST = "127.0.0.1"
|
||||||
PORT = 4000
|
PORT = 4000
|
||||||
@@ -598,6 +599,7 @@ def apply_update_stub():
|
|||||||
|
|
||||||
manifest = None
|
manifest = None
|
||||||
state["in_progress"] = True
|
state["in_progress"] = True
|
||||||
|
state["status"] = "in_progress"
|
||||||
state["progress"] = "Starting update…"
|
state["progress"] = "Starting update…"
|
||||||
save_update_state(state)
|
save_update_state(state)
|
||||||
|
|
||||||
@@ -642,7 +644,8 @@ def apply_update_stub():
|
|||||||
ensure_dir(backup_dir)
|
ensure_dir(backup_dir)
|
||||||
# Backup web and api
|
# Backup web and api
|
||||||
if WEB_ROOT.exists():
|
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():
|
if API_PATH.exists():
|
||||||
shutil.copy2(API_PATH, backup_dir / "pikit-api.py")
|
shutil.copy2(API_PATH, backup_dir / "pikit-api.py")
|
||||||
|
|
||||||
@@ -672,7 +675,7 @@ def apply_update_stub():
|
|||||||
state["progress"] = None
|
state["progress"] = None
|
||||||
save_update_state(state)
|
save_update_state(state)
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
state["status"] = "up_to_date"
|
state["status"] = "error"
|
||||||
state["message"] = f"No release available ({e.code})"
|
state["message"] = f"No release available ({e.code})"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
state["status"] = "error"
|
state["status"] = "error"
|
||||||
@@ -714,17 +717,24 @@ def rollback_update_stub():
|
|||||||
state["message"] = "Another update is running"
|
state["message"] = "Another update is running"
|
||||||
save_update_state(state)
|
save_update_state(state)
|
||||||
return 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)
|
backups = sorted(BACKUP_ROOT.glob("*"), reverse=True)
|
||||||
if not backups:
|
if not backups:
|
||||||
state["status"] = "error"
|
state["status"] = "error"
|
||||||
state["message"] = "No backup available to rollback."
|
state["message"] = "No backup available to rollback."
|
||||||
|
state["in_progress"] = False
|
||||||
|
state["progress"] = None
|
||||||
save_update_state(state)
|
save_update_state(state)
|
||||||
|
release_lock(lock)
|
||||||
return state
|
return state
|
||||||
target = backups[0]
|
target = backups[0]
|
||||||
try:
|
try:
|
||||||
if (target / "pikit-web").exists():
|
if (target / "pikit-web").exists():
|
||||||
shutil.rmtree(WEB_ROOT, ignore_errors=True)
|
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():
|
if (target / "pikit-api.py").exists():
|
||||||
shutil.copy2(target / "pikit-api.py", API_PATH)
|
shutil.copy2(target / "pikit-api.py", API_PATH)
|
||||||
os.chmod(API_PATH, 0o755)
|
os.chmod(API_PATH, 0o755)
|
||||||
@@ -735,9 +745,10 @@ def rollback_update_stub():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
state["status"] = "error"
|
state["status"] = "error"
|
||||||
state["message"] = f"Rollback failed: {e}"
|
state["message"] = f"Rollback failed: {e}"
|
||||||
|
state["in_progress"] = False
|
||||||
|
state["progress"] = None
|
||||||
save_update_state(state)
|
save_update_state(state)
|
||||||
if lock:
|
release_lock(lock)
|
||||||
release_lock(lock)
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ const busyOverlay = document.getElementById("busyOverlay");
|
|||||||
const busyTitle = document.getElementById("busyTitle");
|
const busyTitle = document.getElementById("busyTitle");
|
||||||
const busyText = document.getElementById("busyText");
|
const busyText = document.getElementById("busyText");
|
||||||
const toastContainer = document.getElementById("toastContainer");
|
const toastContainer = document.getElementById("toastContainer");
|
||||||
|
const readyOverlay = document.getElementById("readyOverlay");
|
||||||
|
|
||||||
const TOAST_POS_KEY = "pikit-toast-pos";
|
const TOAST_POS_KEY = "pikit-toast-pos";
|
||||||
const TOAST_ANIM_KEY = "pikit-toast-anim";
|
const TOAST_ANIM_KEY = "pikit-toast-anim";
|
||||||
@@ -398,6 +399,7 @@ async function loadReleaseStatus() {
|
|||||||
auto_check = false,
|
auto_check = false,
|
||||||
progress = null,
|
progress = null,
|
||||||
} = data || {};
|
} = data || {};
|
||||||
|
window.__lastReleaseState = data;
|
||||||
setReleaseChip(data);
|
setReleaseChip(data);
|
||||||
if (releaseCurrent) releaseCurrent.textContent = current_version;
|
if (releaseCurrent) releaseCurrent.textContent = current_version;
|
||||||
if (releaseLatest) releaseLatest.textContent = latest_version;
|
if (releaseLatest) releaseLatest.textContent = latest_version;
|
||||||
@@ -485,9 +487,10 @@ function wireReleaseControls() {
|
|||||||
|
|
||||||
releaseApplyBtn?.addEventListener("click", async () => {
|
releaseApplyBtn?.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
|
showBusy("Updating Pi-Kit…", "Applying release. This can take up to a minute.");
|
||||||
if (releaseProgress) releaseProgress.textContent = "Downloading and installing…";
|
if (releaseProgress) releaseProgress.textContent = "Downloading and installing…";
|
||||||
await applyRelease();
|
await applyRelease();
|
||||||
await loadReleaseStatus();
|
pollReleaseStatus();
|
||||||
showToast("Update started", "success");
|
showToast("Update started", "success");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.error || "Update failed", "error");
|
showToast(e.error || "Update failed", "error");
|
||||||
@@ -498,10 +501,11 @@ function wireReleaseControls() {
|
|||||||
|
|
||||||
releaseRollbackBtn?.addEventListener("click", async () => {
|
releaseRollbackBtn?.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
|
showBusy("Rolling back…", "Restoring previous backup.");
|
||||||
if (releaseProgress) releaseProgress.textContent = "Rolling back…";
|
if (releaseProgress) releaseProgress.textContent = "Rolling back…";
|
||||||
await rollbackRelease();
|
await rollbackRelease();
|
||||||
await loadReleaseStatus();
|
pollReleaseStatus();
|
||||||
showToast("Rollback complete", "success");
|
showToast("Rollback started", "success");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.error || "Rollback failed", "error");
|
showToast(e.error || "Rollback failed", "error");
|
||||||
} finally {
|
} 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.") {
|
function showBusy(title = "Working…", text = "This may take a few seconds.") {
|
||||||
if (!busyOverlay) return;
|
if (!busyOverlay) return;
|
||||||
busyTitle.textContent = title;
|
busyTitle.textContent = title;
|
||||||
|
|||||||
@@ -1374,6 +1374,11 @@ select:focus-visible,
|
|||||||
#addServiceModal .controls {
|
#addServiceModal .controls {
|
||||||
padding: 0 2px 4px;
|
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 {
|
.modal:not(.hidden) .modal-card {
|
||||||
transform: translateY(0) scale(1);
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user