diff --git a/pikit-api.py b/pikit-api.py
index bbfc536..9d841fe 100644
--- a/pikit-api.py
+++ b/pikit-api.py
@@ -14,7 +14,7 @@ import argparse
import sys
from pikit_api import HOST, PORT
-from pikit_api.releases import apply_update, check_for_update, rollback_update
+from pikit_api.releases import apply_update, check_for_update
from pikit_api.server import run_server
@@ -22,7 +22,6 @@ def parse_args():
parser = argparse.ArgumentParser(description="Pi-Kit API / updater")
parser.add_argument("--apply-update", action="store_true", help="Apply latest release (non-HTTP mode)")
parser.add_argument("--check-update", action="store_true", help="Check for latest release (non-HTTP mode)")
- parser.add_argument("--rollback-update", action="store_true", help="Rollback to last backup (non-HTTP mode)")
parser.add_argument("--host", default=HOST, help="Bind host (default 127.0.0.1)")
parser.add_argument("--port", type=int, default=PORT, help="Bind port (default 4000)")
return parser.parse_args()
@@ -36,9 +35,6 @@ def main():
if args.check_update:
check_for_update()
return
- if args.rollback_update:
- rollback_update()
- return
run_server(host=args.host, port=args.port)
diff --git a/pikit-web/assets/api.js b/pikit-web/assets/api.js
index a0dad2a..5ecd348 100644
--- a/pikit-web/assets/api.js
+++ b/pikit-web/assets/api.js
@@ -53,10 +53,12 @@ export const applyRelease = () =>
api("/api/update/apply", {
method: "POST",
});
-export const rollbackRelease = () =>
- api("/api/update/rollback", {
+export const applyReleaseVersion = (version) =>
+ api("/api/update/apply_version", {
method: "POST",
+ body: JSON.stringify({ version }),
});
+export const listReleases = () => api("/api/update/releases");
export const setReleaseAutoCheck = (enable) =>
api("/api/update/auto", {
method: "POST",
diff --git a/pikit-web/assets/releases.js b/pikit-web/assets/releases.js
index 55a233c..ec4a9fb 100644
--- a/pikit-web/assets/releases.js
+++ b/pikit-web/assets/releases.js
@@ -5,7 +5,8 @@ import {
getReleaseStatus,
checkRelease,
applyRelease,
- rollbackRelease,
+ applyReleaseVersion,
+ listReleases,
setReleaseAutoCheck,
setReleaseChannel,
} from "./api.js";
@@ -24,7 +25,8 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction, lo
const releaseProgress = document.getElementById("releaseProgress");
const releaseCheckBtn = document.getElementById("releaseCheckBtn");
const releaseApplyBtn = document.getElementById("releaseApplyBtn");
- const releaseRollbackBtn = document.getElementById("releaseRollbackBtn");
+ const releaseVersionSelect = document.getElementById("releaseVersionSelect");
+ const releaseApplyVersionBtn = document.getElementById("releaseApplyVersionBtn");
const releaseAutoCheck = document.getElementById("releaseAutoCheck");
const releaseLog = document.getElementById("releaseLog");
const releaseLogStatus = document.getElementById("releaseLogStatus");
@@ -46,6 +48,7 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction, lo
let changelogCache = { version: null, text: "" };
let lastChangelogUrl = null;
let releaseChannel = "dev";
+ let releaseOptions = [];
const logger = createReleaseLogger(logUi);
logger.attach(releaseLog);
@@ -64,6 +67,41 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction, lo
}
};
+ async function loadReleaseList() {
+ if (!releaseVersionSelect) return;
+ try {
+ const data = await listReleases();
+ releaseOptions = data.releases || [];
+ releaseVersionSelect.innerHTML = "";
+ if (!releaseOptions.length) {
+ const opt = document.createElement("option");
+ opt.value = "";
+ opt.textContent = "No releases found";
+ releaseVersionSelect.appendChild(opt);
+ releaseVersionSelect.disabled = true;
+ releaseApplyVersionBtn && (releaseApplyVersionBtn.disabled = true);
+ return;
+ }
+ releaseVersionSelect.disabled = false;
+ releaseOptions.forEach((r) => {
+ const opt = document.createElement("option");
+ opt.value = r.version;
+ const tag = r.prerelease ? " (dev)" : "";
+ opt.textContent = `${r.version}${tag}${r.published_at ? ` — ${fmtDate(r.published_at)}` : ""}`;
+ releaseVersionSelect.appendChild(opt);
+ });
+ if (releaseApplyVersionBtn) releaseApplyVersionBtn.disabled = false;
+ } catch (e) {
+ releaseVersionSelect.innerHTML = "";
+ const opt = document.createElement("option");
+ opt.value = "";
+ opt.textContent = "Failed to load releases";
+ releaseVersionSelect.appendChild(opt);
+ releaseVersionSelect.disabled = true;
+ if (releaseApplyVersionBtn) releaseApplyVersionBtn.disabled = true;
+ }
+ }
+
function setReleaseChip(state) {
if (!releaseFlagTop) return;
releaseFlagTop.textContent = "Pi-Kit: n/a";
@@ -237,6 +275,7 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction, lo
releaseBtn?.addEventListener("click", () => {
releaseModal?.classList.remove("hidden");
loadReleaseStatus(true);
+ loadReleaseList();
});
releaseClose?.addEventListener("click", () => releaseModal?.classList.add("hidden"));
// Do not allow dismiss by clicking backdrop (consistency with other modals)
@@ -299,19 +338,24 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction, lo
}
});
- releaseRollbackBtn?.addEventListener("click", async () => {
+ releaseApplyVersionBtn?.addEventListener("click", async () => {
+ if (!releaseVersionSelect || !releaseVersionSelect.value) {
+ showToast("Select a version first", "error");
+ return;
+ }
try {
lastReleaseToastKey = null;
- logUi("Rollback requested");
+ const ver = releaseVersionSelect.value;
+ logUi(`Install version ${ver} requested`);
releaseBusyActive = true;
- showBusy("Rolling back…", "Restoring previous backup.");
- logRelease("Starting rollback…");
- await rollbackRelease();
+ showBusy(`Installing ${ver}…`, "Applying selected release. This can take up to a minute.");
+ logRelease(`Installing ${ver}…`);
+ await applyReleaseVersion(ver);
pollReleaseStatus();
- showToast("Rollback started", "success");
+ showToast(`Installing ${ver}`, "success");
} catch (e) {
- showToast(e.error || "Rollback failed", "error");
- logRelease(`Error: ${e.error || "Rollback failed"}`);
+ showToast(e.error || "Install failed", "error");
+ logRelease(`Error: ${e.error || "Install failed"}`);
} finally {
if (releaseProgress) releaseProgress.textContent = "";
}
@@ -334,6 +378,7 @@ export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction, lo
releaseChannel = chan;
logRelease(`Channel set to ${chan}`);
await loadReleaseStatus(true);
+ await loadReleaseList();
} catch (e) {
showToast(e.error || "Failed to save channel", "error");
releaseChannelToggle.checked = releaseChannel === "dev";
diff --git a/pikit-web/index.html b/pikit-web/index.html
index f4015fc..053dd7b 100644
--- a/pikit-web/index.html
+++ b/pikit-web/index.html
@@ -165,8 +165,9 @@
-