diff --git a/pikit-web/assets/main.js b/pikit-web/assets/main.js index bbc0930..1e1869d 100644 --- a/pikit-web/assets/main.js +++ b/pikit-web/assets/main.js @@ -1,19 +1,11 @@ // Entry point for the dashboard: wires UI events, pulls status, and initializes // feature modules (services, settings, stats). -import { - getStatus, - triggerReset, - getReleaseStatus, - checkRelease, - applyRelease, - rollbackRelease, - setReleaseAutoCheck, - setReleaseChannel, -} from "./api.js"; +import { getStatus, triggerReset } from "./api.js"; import { placeholderStatus, renderStats } from "./status.js"; import { initServiceControls, renderServices } from "./services.js"; import { initSettings } from "./settings.js"; import { initUpdateSettings, isUpdatesDirty } from "./update-settings.js"; +import { initReleaseUI } from "./releases.js?v=20251213f"; const servicesGrid = document.getElementById("servicesGrid"); const heroStats = document.getElementById("heroStats"); @@ -84,25 +76,6 @@ const menuClose = document.getElementById("menuClose"); const advBtn = document.getElementById("advBtn"); const advModal = document.getElementById("advModal"); const advClose = document.getElementById("advClose"); -const releaseBtn = document.getElementById("releaseBtn"); -const releaseModal = document.getElementById("releaseModal"); -const releaseClose = document.getElementById("releaseClose"); -const releaseCurrent = document.getElementById("releaseCurrent"); -const releaseLatest = document.getElementById("releaseLatest"); -const releaseStatusMsg = document.getElementById("releaseStatusMsg"); -const releaseProgress = document.getElementById("releaseProgress"); -const releaseCheckBtn = document.getElementById("releaseCheckBtn"); -const releaseApplyBtn = document.getElementById("releaseApplyBtn"); -const releaseRollbackBtn = document.getElementById("releaseRollbackBtn"); -const releaseAutoCheck = document.getElementById("releaseAutoCheck"); -const releaseLog = document.getElementById("releaseLog"); -const releaseLogStatus = document.getElementById("releaseLogStatus"); -const releaseLogCopy = document.getElementById("releaseLogCopy"); -// Guard against double-loading (cache/tunnel quirks) -const releaseChangelogBtn = window.__pikitReleaseChangelogBtn || document.getElementById("releaseChangelogBtn"); -const releaseChannelToggle = window.__pikitReleaseChannelToggle || document.getElementById("releaseChannelToggle"); -window.__pikitReleaseChangelogBtn = releaseChangelogBtn; -window.__pikitReleaseChannelToggle = releaseChannelToggle; const helpBtn = document.getElementById("helpBtn"); const helpModal = document.getElementById("helpModal"); @@ -146,18 +119,7 @@ let toastAnimation = "slide-in"; let toastDurationMs = 5000; let toastSpeedMs = 300; let fontChoice = "redhat"; -let releaseBusyActive = false; -let releaseLogLines = []; -let lastReleaseLogKey = ""; -let releaseLastFetched = 0; -let changelogCache = { version: null, text: "" }; -let lastChangelogUrl = null; -let releaseChannel = "dev"; - -function shorten(text, max = 90) { - if (!text || typeof text !== "string") return text; - return text.length > max ? `${text.slice(0, max - 3)}...` : text; -} +let releaseUI = null; function applyToastSettings() { if (!toastContainer) return; @@ -380,7 +342,7 @@ async function loadStatus() { } } // Pull Pi-Kit release status after core status - loadReleaseStatus(); + releaseUI?.refreshStatus(); } catch (e) { console.error(e); renderStats(heroStats, placeholderStatus); @@ -417,114 +379,6 @@ function updatesFlagEl(enabled) { if (enabled === false) updatesFlagTop.classList.add("chip-off"); } -async function loadReleaseStatus(force = false) { - if (!releaseFlagTop) return; - const now = Date.now(); - if (!force && now - releaseLastFetched < 60000 && !releaseBusyActive) { - return; - } - setReleaseChip({ status: "checking" }); - try { - const data = await getReleaseStatus(); - const { - current_version = "n/a", - latest_version = "n/a", - status = "unknown", - message = "", - auto_check = false, - progress = null, - channel = "dev", - } = data || {}; - releaseChannel = channel || "dev"; - if (releaseChannelToggle) releaseChannelToggle.checked = releaseChannel === "dev"; - window.__lastReleaseState = data; - const key = [status, progress, message].join("|"); - if (key !== lastReleaseLogKey) { - logRelease(`Status: ${status}${progress ? " • " + progress : ""}${message ? " • " + message : ""}`); - lastReleaseLogKey = key; - } - releaseLastFetched = now; - if (status === "update_available" && message && message.startsWith("http")) { - lastChangelogUrl = message; - } else if (latest_version) { - lastChangelogUrl = `https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v${latest_version}/CHANGELOG-${latest_version}.txt`; - } - setReleaseChip(data); - if (releaseCurrent) releaseCurrent.textContent = current_version; - if (releaseLatest) releaseLatest.textContent = latest_version; - if (releaseStatusMsg) { - releaseStatusMsg.textContent = - status === "update_available" - ? message || "Update available" - : status === "up_to_date" - ? "Up to date" - : message || status; - releaseStatusMsg.classList.toggle("error", status === "error"); - } - if (releaseAutoCheck) releaseAutoCheck.checked = !!auto_check; - if (releaseProgress) { - releaseProgress.textContent = progress ? progress : ""; - } - if (status === "in_progress" && !releaseBusyActive) { - releaseBusyActive = true; - showBusy("Working on update…", progress || "This can take up to a minute."); - pollReleaseStatus(); - } - } catch (e) { - console.error("Failed to load release status", e); - setReleaseChip({ status: "error", message: "Failed to load" }); - if (releaseStatusMsg) { - releaseStatusMsg.textContent = "Failed to load release status"; - releaseStatusMsg.classList.add("error"); - } - } -} - -function setReleaseChip(state) { - if (!releaseFlagTop) return; - releaseFlagTop.textContent = "Pi-Kit: n/a"; - releaseFlagTop.className = "status-chip quiet"; - if (!state) return; - const { status, latest_version, current_version, message } = state; - const label = - status === "update_available" - ? `Update → ${latest_version || "new"}` - : status === "up_to_date" - ? `Pi-Kit: ${current_version || "latest"}` - : status === "checking" - ? "Checking…" - : status === "error" - ? "Update error" - : `Pi-Kit: ${current_version || "n/a"}`; - releaseFlagTop.textContent = label; - if (status === "update_available") releaseFlagTop.classList.add("chip-warm"); - if (status === "error") releaseFlagTop.classList.add("chip-off"); - const msg = shorten(message, 80) || ""; - releaseFlagTop.title = msg || "Pi-Kit release status"; - if (releaseStatusMsg) { - releaseStatusMsg.textContent = - status === "update_available" - ? msg || "Update available" - : status === "up_to_date" - ? msg || "Up to date" - : msg || status; - } - if (releaseLogStatus) { - releaseLogStatus.textContent = - status === "in_progress" - ? "Running…" - : status === "update_available" - ? "Update available" - : status === "error" - ? "Error" - : "Idle"; - } - if (releaseChangelogBtn) { - releaseChangelogBtn.disabled = status === "checking"; - releaseChangelogBtn.classList.toggle("ghost", status !== "update_available"); - } -} - function wireModals() { advBtn.onclick = () => advModal.classList.remove("hidden"); advClose.onclick = () => advModal.classList.add("hidden"); @@ -538,177 +392,6 @@ function wireModals() { addServiceModal?.addEventListener("click", (e) => { if (e.target === addServiceModal) addServiceModal.classList.add("hidden"); }); - releaseBtn?.addEventListener("click", () => { - releaseModal?.classList.remove("hidden"); - loadReleaseStatus(); - }); - releaseClose?.addEventListener("click", () => releaseModal?.classList.add("hidden")); - releaseModal?.addEventListener("click", (e) => { - if (e.target === releaseModal) releaseModal.classList.add("hidden"); - }); -} - -function wireReleaseControls() { - releaseCheckBtn?.addEventListener("click", async () => { - try { - if (releaseProgress) releaseProgress.textContent = "Checking for updates…"; - logRelease("Checking for updates…"); - await checkRelease(); - await loadReleaseStatus(true); - const state = window.__lastReleaseState || {}; - logRelease( - `Status: ${state.status || "unknown"}${ - state.message ? " • " + state.message : "" - }`, - ); - showToast("Checked for updates", "success"); - } catch (e) { - showToast(e.error || "Check failed", "error"); - logRelease(`Error: ${e.error || "Check failed"}`); - } finally { - if (releaseProgress) releaseProgress.textContent = ""; - } - }); - - releaseApplyBtn?.addEventListener("click", async () => { - try { - const state = window.__lastReleaseState || {}; - const { current_version, latest_version } = state; - const sameVersion = - current_version && - latest_version && - String(current_version) === String(latest_version); - if (sameVersion) { - const proceed = await confirmAction( - "Reinstall same version?", - `You are already on ${current_version}. Reinstall this version anyway?`, - ); - if (!proceed) { - logRelease("Upgrade cancelled (same version)."); - return; - } - } - releaseBusyActive = true; - showBusy("Updating Pi-Kit…", "Applying release. This can take up to a minute."); - if (releaseProgress) releaseProgress.textContent = "Downloading and installing…"; - logRelease("Starting upgrade…"); - await applyRelease(); - pollReleaseStatus(); - showToast("Update started", "success"); - } catch (e) { - showToast(e.error || "Update failed", "error"); - logRelease(`Error: ${e.error || "Update failed"}`); - } finally { - if (releaseProgress) releaseProgress.textContent = ""; - } - }); - - releaseRollbackBtn?.addEventListener("click", async () => { - try { - releaseBusyActive = true; - showBusy("Rolling back…", "Restoring previous backup."); - if (releaseProgress) releaseProgress.textContent = "Rolling back…"; - logRelease("Starting rollback…"); - await rollbackRelease(); - pollReleaseStatus(); - showToast("Rollback started", "success"); - } catch (e) { - showToast(e.error || "Rollback failed", "error"); - logRelease(`Error: ${e.error || "Rollback failed"}`); - } finally { - if (releaseProgress) releaseProgress.textContent = ""; - } - }); - - releaseAutoCheck?.addEventListener("change", async () => { - try { - await setReleaseAutoCheck(releaseAutoCheck.checked); - showToast("Auto-check preference saved", "success"); - } catch (e) { - showToast(e.error || "Failed to save preference", "error"); - releaseAutoCheck.checked = !releaseAutoCheck.checked; - } - }); - - releaseChannelToggle?.addEventListener("change", async () => { - try { - const chan = releaseChannelToggle.checked ? "dev" : "stable"; - await setReleaseChannel(chan); - releaseChannel = chan; - logRelease(`Channel set to ${chan}`); - await loadReleaseStatus(true); - } catch (e) { - showToast(e.error || "Failed to save channel", "error"); - releaseChannelToggle.checked = releaseChannel === "dev"; - } - }); - - releaseChangelogBtn?.addEventListener("click", async () => { - const state = window.__lastReleaseState || {}; - const { latest_version, message } = state; - const url = (message && message.startsWith("http") ? message : null) || lastChangelogUrl; - if (!url) { - showToast("No changelog URL available", "error"); - return; - } - await showChangelog(latest_version, url); - }); - - releaseLogCopy?.addEventListener("click", async () => { - try { - const text = releaseLogLines.join("\n") || "No log entries yet."; - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(text); - } else { - // Fallback for non-HTTPS contexts - const ta = document.createElement("textarea"); - ta.value = text; - ta.style.position = "fixed"; - ta.style.opacity = "0"; - document.body.appendChild(ta); - ta.select(); - document.execCommand("copy"); - document.body.removeChild(ta); - } - showToast("Log copied", "success"); - } catch (e) { - console.error("Copy failed", e); - showToast("Could not copy log", "error"); - } - }); - - changelogClose?.addEventListener("click", () => { - changelogModal?.classList.add("hidden"); - releaseModal?.classList.remove("hidden"); - }); -} - -function pollReleaseStatus() { - let attempts = 0; - const maxAttempts = 30; // ~1 min at 2s - const started = Date.now(); - const minWaitMs = 3000; - const tick = async () => { - attempts += 1; - await loadReleaseStatus(true); - const state = window.__lastReleaseState || {}; - const tooSoon = Date.now() - started < minWaitMs; - if ((state.status === "in_progress" || tooSoon) && attempts < maxAttempts) { - setTimeout(tick, 1000); - } else { - releaseBusyActive = false; - hideBusy(); - if (releaseProgress) releaseProgress.textContent = ""; - if (state.status === "up_to_date") { - showToast(state.message || "Update complete", "success"); - logRelease("Update complete"); - } else if (state.status === "error") { - showToast(state.message || "Update failed", "error"); - logRelease(`Error: ${state.message || "Update failed"}`); - } - } - }; - tick(); } function showBusy(title = "Working…", text = "This may take a few seconds.") { @@ -723,47 +406,6 @@ function hideBusy() { busyOverlay?.classList.add("hidden"); } -function logRelease(msg) { - if (!msg) return; - const ts = new Date().toLocaleTimeString(); - const line = `${ts} ${msg}`; - if (releaseLogLines[0] === line) return; - releaseLogLines.unshift(line); - releaseLogLines = releaseLogLines.slice(0, 80); - if (releaseLog) releaseLog.textContent = releaseLogLines.join("\n"); -} - -async function fetchText(url) { - const res = await fetch(url); - if (!res.ok) throw new Error(`Fetch failed (${res.status})`); - return res.text(); -} - -async function showChangelog(version, url) { - if (!changelogBody || !changelogModal) { - window.open(url, "_blank"); - return; - } - try { - if (changelogCache.version === version && changelogCache.text) { - changelogBody.textContent = changelogCache.text; - } else { - changelogBody.textContent = "Loading changelog…"; - const res = await fetch(`/api/update/changelog?${url ? `url=${encodeURIComponent(url)}` : ""}`); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = await res.json(); - const text = data.text || "No changelog content."; - changelogCache = { version, text }; - changelogBody.textContent = text; - } - changelogTitle.textContent = version ? `Changelog — ${version}` : "Changelog"; - changelogModal.classList.remove("hidden"); - } catch (e) { - console.error("Changelog fetch failed", e); - showToast("Failed to load changelog", "error"); - } -} - function confirmAction(title, body) { return new Promise((resolve) => { if (!confirmModal) { @@ -846,9 +488,14 @@ if (typeof window !== "undefined") { function main() { applyTooltips(); wireModals(); - wireReleaseControls(); wireResetAndUpdates(); wireAccordions(); + releaseUI = initReleaseUI({ + showToast, + showBusy, + hideBusy, + confirmAction, + }); loadToastSettings(); if (advClose) { diff --git a/pikit-web/assets/releases.js b/pikit-web/assets/releases.js new file mode 100644 index 0000000..a1ad78d --- /dev/null +++ b/pikit-web/assets/releases.js @@ -0,0 +1,363 @@ +// Release / updater UI controller +// Handles checking, applying, rollback, channel toggle, changelog modal, and log rendering. + +import { + getReleaseStatus, + checkRelease, + applyRelease, + rollbackRelease, + setReleaseAutoCheck, + setReleaseChannel, +} from "./api.js"; + +function shorten(text, max = 90) { + if (!text || typeof text !== "string") return text; + return text.length > max ? `${text.slice(0, max - 3)}...` : text; +} + +export function initReleaseUI({ showToast, showBusy, hideBusy, confirmAction }) { + const releaseFlagTop = document.getElementById("releaseFlagTop"); + const releaseBtn = document.getElementById("releaseBtn"); + const releaseModal = document.getElementById("releaseModal"); + const releaseClose = document.getElementById("releaseClose"); + const releaseCurrent = document.getElementById("releaseCurrent"); + const releaseLatest = document.getElementById("releaseLatest"); + const releaseStatusMsg = document.getElementById("releaseStatusMsg"); + const releaseProgress = document.getElementById("releaseProgress"); + const releaseCheckBtn = document.getElementById("releaseCheckBtn"); + const releaseApplyBtn = document.getElementById("releaseApplyBtn"); + const releaseRollbackBtn = document.getElementById("releaseRollbackBtn"); + const releaseAutoCheck = document.getElementById("releaseAutoCheck"); + const releaseLog = document.getElementById("releaseLog"); + const releaseLogStatus = document.getElementById("releaseLogStatus"); + const releaseLogCopy = document.getElementById("releaseLogCopy"); + const releaseChangelogBtn = window.__pikitReleaseChangelogBtn || document.getElementById("releaseChangelogBtn"); + const releaseChannelToggle = window.__pikitReleaseChannelToggle || document.getElementById("releaseChannelToggle"); + window.__pikitReleaseChangelogBtn = releaseChangelogBtn; + window.__pikitReleaseChannelToggle = releaseChannelToggle; + + const changelogModal = document.getElementById("changelogModal"); + const changelogTitle = document.getElementById("changelogTitle"); + const changelogBody = document.getElementById("changelogBody"); + const changelogClose = document.getElementById("changelogClose"); + + let releaseBusyActive = false; + let releaseLogLines = []; + let releaseLastFetched = 0; + let lastReleaseLogKey = ""; + let changelogCache = { version: null, text: "" }; + let lastChangelogUrl = null; + let releaseChannel = "dev"; + + function logRelease(msg) { + if (!msg) return; + const ts = new Date().toLocaleTimeString(); + const line = `${ts} ${msg}`; + if (releaseLogLines[0] === line) return; + releaseLogLines.unshift(line); + releaseLogLines = releaseLogLines.slice(0, 120); + if (releaseLog) { + releaseLog.textContent = releaseLogLines.join("\n"); + releaseLog.scrollTop = 0; // keep most recent in view + } + } + + function setReleaseChip(state) { + if (!releaseFlagTop) return; + releaseFlagTop.textContent = "Pi-Kit: n/a"; + releaseFlagTop.className = "status-chip quiet"; + if (!state) return; + const { status, latest_version, current_version, message } = state; + const label = + status === "update_available" + ? `Update → ${latest_version || "new"}` + : status === "up_to_date" + ? `Pi-Kit: ${current_version || "latest"}` + : status === "checking" + ? "Checking…" + : status === "error" + ? "Update error" + : `Pi-Kit: ${current_version || "n/a"}`; + releaseFlagTop.textContent = label; + if (status === "update_available") releaseFlagTop.classList.add("chip-warm"); + if (status === "error") releaseFlagTop.classList.add("chip-off"); + const msg = shorten(message, 80) || ""; + releaseFlagTop.title = msg || "Pi-Kit release status"; + if (releaseStatusMsg) { + releaseStatusMsg.textContent = + status === "update_available" + ? msg || "Update available" + : status === "up_to_date" + ? msg || "Up to date" + : msg || status; + } + if (releaseLogStatus) { + releaseLogStatus.textContent = + status === "in_progress" + ? "Running…" + : status === "update_available" + ? "Update available" + : status === "error" + ? "Error" + : "Idle"; + } + if (releaseChangelogBtn) { + releaseChangelogBtn.disabled = status === "checking"; + releaseChangelogBtn.classList.toggle("ghost", status !== "update_available"); + } + } + + async function showChangelog(version, url) { + if (!changelogBody || !changelogModal) { + window.open(url, "_blank"); + return; + } + try { + if (changelogCache.version === version && changelogCache.text) { + changelogBody.textContent = changelogCache.text; + } else { + changelogBody.textContent = "Loading changelog…"; + const res = await fetch(`/api/update/changelog?${url ? `url=${encodeURIComponent(url)}` : ""}`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + const text = data.text || "No changelog content."; + changelogCache = { version, text }; + changelogBody.textContent = text; + } + changelogTitle.textContent = version ? `Changelog — ${version}` : "Changelog"; + releaseModal?.classList.add("hidden"); + changelogModal.classList.remove("hidden"); + } catch (e) { + console.error("Changelog fetch failed", e); + showToast("Failed to load changelog", "error"); + } + } + + async function loadReleaseStatus(force = false) { + if (!releaseFlagTop) return; + const now = Date.now(); + if (!force && now - releaseLastFetched < 60000 && !releaseBusyActive) { + return; + } + setReleaseChip({ status: "checking" }); + try { + const data = await getReleaseStatus(); + const { + current_version = "n/a", + latest_version = "n/a", + status = "unknown", + message = "", + auto_check = false, + progress = null, + channel = "dev", + } = data || {}; + releaseChannel = channel || "dev"; + if (releaseChannelToggle) releaseChannelToggle.checked = releaseChannel === "dev"; + window.__lastReleaseState = data; + const key = [status, progress, message].join("|"); + if (key !== lastReleaseLogKey) { + logRelease(`Status: ${status}${progress ? " • " + progress : ""}${message ? " • " + message : ""}`); + lastReleaseLogKey = key; + } + releaseLastFetched = now; + if (status === "update_available" && message && message.startsWith("http")) { + lastChangelogUrl = message; + } else if (latest_version) { + lastChangelogUrl = `https://git.44r0n.cc/44r0n7/pi-kit/releases/download/v${latest_version}/CHANGELOG-${latest_version}.txt`; + } + setReleaseChip(data); + if (releaseCurrent) releaseCurrent.textContent = current_version; + if (releaseLatest) releaseLatest.textContent = latest_version; + if (releaseAutoCheck) releaseAutoCheck.checked = !!auto_check; + if (releaseProgress) releaseProgress.textContent = progress || ""; + if (status === "in_progress" && progress) { + showBusy("Working on update…", progress || "This can take up to a minute."); + pollReleaseStatus(); + } + } catch (e) { + console.error("Failed to load release status", e); + setReleaseChip({ status: "error", message: "Failed to load" }); + if (releaseStatusMsg) { + releaseStatusMsg.textContent = "Failed to load release status"; + releaseStatusMsg.classList.add("error"); + } + } + } + + function pollReleaseStatus() { + let attempts = 0; + const maxAttempts = 30; // ~1 min at 1s + const started = Date.now(); + const minWaitMs = 3000; + const tick = async () => { + attempts += 1; + await loadReleaseStatus(true); + const state = window.__lastReleaseState || {}; + const tooSoon = Date.now() - started < minWaitMs; + if ((state.status === "in_progress" || tooSoon) && attempts < maxAttempts) { + setTimeout(tick, 1000); + } else { + releaseBusyActive = false; + hideBusy(); + if (releaseProgress) releaseProgress.textContent = ""; + if (state.status === "up_to_date") { + showToast(state.message || "Update complete", "success"); + logRelease("Update complete"); + } else if (state.status === "error") { + showToast(state.message || "Update failed", "error"); + logRelease(`Error: ${state.message || "Update failed"}`); + } + } + }; + tick(); + } + + function wireReleaseControls() { + releaseBtn?.addEventListener("click", () => { + releaseModal?.classList.remove("hidden"); + loadReleaseStatus(true); + }); + releaseClose?.addEventListener("click", () => releaseModal?.classList.add("hidden")); + releaseModal?.addEventListener("click", (e) => { + if (e.target === releaseModal) releaseModal.classList.add("hidden"); + }); + + releaseCheckBtn?.addEventListener("click", async () => { + try { + if (releaseProgress) releaseProgress.textContent = "Checking for updates…"; + logRelease("Checking for updates…"); + await checkRelease(); + await loadReleaseStatus(true); + const state = window.__lastReleaseState || {}; + logRelease( + `Status: ${state.status || "unknown"}${state.message ? " • " + state.message : ""}`, + ); + showToast("Checked for updates", "success"); + } catch (e) { + showToast(e.error || "Check failed", "error"); + logRelease(`Error: ${e.error || "Check failed"}`); + } finally { + if (releaseProgress) releaseProgress.textContent = ""; + } + }); + + releaseApplyBtn?.addEventListener("click", async () => { + try { + const state = window.__lastReleaseState || {}; + const { current_version, latest_version } = state; + const sameVersion = + current_version && + latest_version && + String(current_version) === String(latest_version); + if (sameVersion) { + const proceed = await confirmAction( + "Reinstall same version?", + `You are already on ${current_version}. Reinstall this version anyway?`, + ); + if (!proceed) { + logRelease("Upgrade cancelled (same version)."); + return; + } + } + releaseBusyActive = true; + showBusy("Updating Pi-Kit…", "Applying release. This can take up to a minute."); + if (releaseProgress) releaseProgress.textContent = "Downloading and installing…"; + logRelease("Starting upgrade…"); + await applyRelease(); + pollReleaseStatus(); + showToast("Update started", "success"); + } catch (e) { + showToast(e.error || "Update failed", "error"); + logRelease(`Error: ${e.error || "Update failed"}`); + } finally { + if (releaseProgress) releaseProgress.textContent = ""; + } + }); + + releaseRollbackBtn?.addEventListener("click", async () => { + try { + releaseBusyActive = true; + showBusy("Rolling back…", "Restoring previous backup."); + if (releaseProgress) releaseProgress.textContent = "Rolling back…"; + logRelease("Starting rollback…"); + await rollbackRelease(); + pollReleaseStatus(); + showToast("Rollback started", "success"); + } catch (e) { + showToast(e.error || "Rollback failed", "error"); + logRelease(`Error: ${e.error || "Rollback failed"}`); + } finally { + if (releaseProgress) releaseProgress.textContent = ""; + } + }); + + releaseAutoCheck?.addEventListener("change", async () => { + try { + await setReleaseAutoCheck(releaseAutoCheck.checked); + showToast("Auto-check preference saved", "success"); + } catch (e) { + showToast(e.error || "Failed to save preference", "error"); + releaseAutoCheck.checked = !releaseAutoCheck.checked; + } + }); + + releaseChannelToggle?.addEventListener("change", async () => { + try { + const chan = releaseChannelToggle.checked ? "dev" : "stable"; + await setReleaseChannel(chan); + releaseChannel = chan; + logRelease(`Channel set to ${chan}`); + await loadReleaseStatus(true); + } catch (e) { + showToast(e.error || "Failed to save channel", "error"); + releaseChannelToggle.checked = releaseChannel === "dev"; + } + }); + + releaseChangelogBtn?.addEventListener("click", async () => { + const state = window.__lastReleaseState || {}; + const { latest_version, message } = state; + const url = (message && message.startsWith("http") ? message : null) || lastChangelogUrl; + if (!url) { + showToast("No changelog URL available", "error"); + return; + } + await showChangelog(latest_version, url); + }); + + releaseLogCopy?.addEventListener("click", async () => { + try { + const text = releaseLogLines.join("\n") || "No log entries yet."; + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + } else { + // Fallback for non-HTTPS contexts + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + } + showToast("Log copied", "success"); + } catch (e) { + console.error("Copy failed", e); + showToast("Could not copy log", "error"); + } + }); + + changelogClose?.addEventListener("click", () => { + changelogModal?.classList.add("hidden"); + releaseModal?.classList.remove("hidden"); + }); + } + + wireReleaseControls(); + + return { + refreshStatus: (force = false) => loadReleaseStatus(force), + logRelease, + }; +} diff --git a/pikit-web/assets/style.css b/pikit-web/assets/style.css index a9acb20..ed8dd44 100644 --- a/pikit-web/assets/style.css +++ b/pikit-web/assets/style.css @@ -417,6 +417,10 @@ body { .modal-card.wide pre.log-box { max-height: 60vh; } +#releaseModal pre.log-box { + max-height: 220px !important; + overflow-y: auto; +} :root[data-theme="light"] .log-box { background: rgba(12, 18, 32, 0.04); } diff --git a/pikit-web/index.html b/pikit-web/index.html index 43efee4..d89e4e7 100644 --- a/pikit-web/index.html +++ b/pikit-web/index.html @@ -726,7 +726,7 @@ - +