diff --git a/classes/plugin/SafeUpgradeManager.php b/classes/plugin/SafeUpgradeManager.php index 5131618a..1cb3f373 100644 --- a/classes/plugin/SafeUpgradeManager.php +++ b/classes/plugin/SafeUpgradeManager.php @@ -606,9 +606,8 @@ class SafeUpgradeManager try { $file = $this->download($package, $timeout); - $this->setProgress('installing', 'Installing update...', 80); $this->performInstall($file); - $this->setProgress('installing', 'Preparing promotion...', 92); + $this->setProgress('installing', 'Preparing promotion...', null); } catch (Throwable $e) { $this->setProgress('error', $e->getMessage(), null); @@ -620,14 +619,14 @@ class SafeUpgradeManager $this->tmp = null; } - $this->setProgress('finalizing', 'Finalizing upgrade...', 95); + $this->setProgress('finalizing', 'Finalizing upgrade...', null); $safeUpgrade->clearRecoveryFlag(); if ($this->recovery && method_exists($this->recovery, 'closeUpgradeWindow')) { $this->recovery->closeUpgradeWindow(); } $this->ensureExecutablePermissions(); - $this->setProgress('finalizing', 'Finalizing upgrade...', 98); + $this->setProgress('finalizing', 'Finalizing upgrade...', null); $manifest = $this->resolveLatestManifest(); @@ -853,7 +852,7 @@ class SafeUpgradeManager */ protected function performInstall(string $zip): void { - $this->setProgress('installing', 'Unpacking archive...', 82); + $this->setProgress('installing', 'Unpacking update...', null); $folder = Installer::unZip($zip, $this->tmp . '/zip'); if ($folder === false) { throw new RuntimeException(Installer::lastErrorMsg()); @@ -870,9 +869,9 @@ class SafeUpgradeManager } try { - $this->setProgress('installing', 'Running installer...', 85); + $this->setProgress('installing', 'Running installer...', null); $install($zip); - $this->setProgress('installing', 'Verifying files...', 88); + $this->setProgress('installing', 'Verifying files...', null); } catch (Throwable $e) { throw new RuntimeException($e->getMessage(), 0, $e); } diff --git a/themes/grav/app/updates/safe-upgrade.js b/themes/grav/app/updates/safe-upgrade.js index 646e6eb0..4aa01544 100644 --- a/themes/grav/app/updates/safe-upgrade.js +++ b/themes/grav/app/updates/safe-upgrade.js @@ -56,6 +56,8 @@ export default class SafeUpgrade { this.statusFailures = 0; this.statusContext = null; this.statusIdleCount = 0; + this.currentStage = null; + this.stageEnteredAt = 0; this.directStatusUrl = this.resolveDirectStatusUrl(); this.preferDirectStatus = !!this.directStatusUrl; @@ -124,6 +126,8 @@ export default class SafeUpgrade { this.preferDirectStatus = !!this.directStatusUrl; this.statusContext = null; this.statusIdleCount = 0; + this.currentStage = null; + this.stageEnteredAt = 0; this.renderLoading(); this.modal.open(); this.fetchPreflight(); @@ -639,6 +643,11 @@ export default class SafeUpgrade { } const stage = data.stage || 'initializing'; + if (stage !== this.currentStage) { + this.currentStage = stage; + this.stageEnteredAt = Date.now(); + } + const titleResolver = STAGE_TITLES[stage] || STAGE_TITLES.initializing; const title = titleResolver(); let percent = typeof data.percent === 'number' ? data.percent : null; @@ -648,12 +657,16 @@ export default class SafeUpgrade { if (stage === 'initializing') { return percent !== null ? Math.min(percent, 5) : 5; } if (stage === 'downloading') { if (percent !== null) { - return Math.min(60, Math.round(10 + (percent * 0.5))); + return Math.min(20, Math.max(5, Math.round(percent * 0.2))); } - return 25; + return 12; + } + if (stage === 'installing') { + return this.computeSmoothPercent(20, 90, 28, percent); + } + if (stage === 'finalizing') { + return this.computeSmoothPercent(90, 99, 6, percent); } - if (stage === 'installing') { return percent !== null ? Math.min(Math.max(percent, 80), 94) : 80; } - if (stage === 'finalizing') { return percent !== null ? Math.min(Math.max(percent, 95), 99) : 95; } if (stage === 'complete') { return 100; } if (stage === 'error') { return null; } return percent; @@ -665,11 +678,14 @@ export default class SafeUpgrade { const statusLine = job && job.status ? `

${t('SAFE_UPGRADE_JOB_STATUS', 'Status')}: ${job.status.toUpperCase()}${job.error ? ` — ${job.error}` : ''}

` : ''; const animateBar = stage !== 'complete' && stage !== 'error' && percent !== null; const barClass = `safe-upgrade-progress-bar${animateBar ? ' is-active' : ''}`; + const detailMessage = stage === 'error' + ? `

${data.message || ''}

` + : (data.message && stage !== 'installing' && stage !== 'finalizing' ? `

${data.message}

` : ''); this.steps.progress.html(`

${title}

-

${data.message || ''}

+ ${detailMessage} ${statusLine} ${percentLabel ? `
${percentLabel}
` : ''}
@@ -746,6 +762,23 @@ export default class SafeUpgrade { this.isPolling = false; this.clearPollTimer(); } + + computeSmoothPercent(base, target, durationSeconds, actualPercent) { + const span = target - base; + if (span <= 0) { + return actualPercent !== null ? Math.min(Math.max(actualPercent, base), target) : base; + } + + const elapsed = Math.max(0, (Date.now() - this.stageEnteredAt) / 1000); + const progressRatio = Math.min(1, elapsed / Math.max(durationSeconds, 1)); + let smooth = base + (progressRatio * span); + + if (actualPercent !== null && !Number.isNaN(actualPercent)) { + smooth = Math.max(smooth, Math.min(actualPercent, target)); + } + + return Math.min(smooth, target); + } } function basename(path) { diff --git a/themes/grav/js/admin.min.js b/themes/grav/js/admin.min.js index 8d48f787..a9cb77b0 100644 --- a/themes/grav/js/admin.min.js +++ b/themes/grav/js/admin.min.js @@ -4612,6 +4612,8 @@ var SafeUpgrade = /*#__PURE__*/function () { this.statusFailures = 0; this.statusContext = null; this.statusIdleCount = 0; + this.currentStage = null; + this.stageEnteredAt = 0; this.directStatusUrl = this.resolveDirectStatusUrl(); this.preferDirectStatus = !!this.directStatusUrl; this.registerEvents(); @@ -4680,6 +4682,8 @@ var SafeUpgrade = /*#__PURE__*/function () { this.preferDirectStatus = !!this.directStatusUrl; this.statusContext = null; this.statusIdleCount = 0; + this.currentStage = null; + this.stageEnteredAt = 0; this.renderLoading(); this.modal.open(); this.fetchPreflight(); @@ -5101,9 +5105,14 @@ var SafeUpgrade = /*#__PURE__*/function () { return; } var stage = data.stage || 'initializing'; + if (stage !== this.currentStage) { + this.currentStage = stage; + this.stageEnteredAt = Date.now(); + } var titleResolver = STAGE_TITLES[stage] || STAGE_TITLES.initializing; var title = titleResolver(); var percent = typeof data.percent === 'number' ? data.percent : null; + var self = this; var scaledPercent = function scaledPercent() { if (stage === 'queued') { return 0; @@ -5113,15 +5122,15 @@ var SafeUpgrade = /*#__PURE__*/function () { } if (stage === 'downloading') { if (percent !== null) { - return Math.min(60, Math.round(10 + percent * 0.5)); + return Math.min(20, Math.max(5, Math.round(percent * 0.2))); } - return 25; + return 12; } if (stage === 'installing') { - return percent !== null ? Math.min(Math.max(percent, 80), 94) : 80; + return self.computeSmoothPercent(20, 90, 28, percent); } if (stage === 'finalizing') { - return percent !== null ? Math.min(Math.max(percent, 95), 99) : 95; + return self.computeSmoothPercent(90, 99, 6, percent); } if (stage === 'complete') { return 100; @@ -5136,7 +5145,8 @@ var SafeUpgrade = /*#__PURE__*/function () { var statusLine = job && job.status ? "

".concat(t('SAFE_UPGRADE_JOB_STATUS', 'Status'), ": ").concat(job.status.toUpperCase(), "").concat(job.error ? " — ".concat(job.error) : '', "

") : ''; var animateBar = stage !== 'complete' && stage !== 'error' && percent !== null; var barClass = "safe-upgrade-progress-bar".concat(animateBar ? ' is-active' : ''); - this.steps.progress.html("\n
\n

".concat(title, "

\n

").concat(data.message || '', "

\n ").concat(statusLine, "\n ").concat(percentLabel ? "
").concat(percentLabel, "
") : '', "\n
\n ")); + var detailMessage = stage === 'error' ? "

".concat(data.message || '', "

") : data.message && stage !== 'installing' && stage !== 'finalizing' ? "

".concat(data.message, "

") : ''; + this.steps.progress.html("\n
\n

".concat(title, "

\n ").concat(detailMessage, "\n ").concat(statusLine, "\n ").concat(percentLabel ? "
").concat(percentLabel, "
") : '', "\n
\n ")); this.switchStep('progress'); if (stage === 'complete') { this.renderResult({ @@ -5187,6 +5197,24 @@ var SafeUpgrade = /*#__PURE__*/function () { _this7.steps[handle].toggleClass('hidden', !isActive); }); } + }, { + key: "computeSmoothPercent", + value: function computeSmoothPercent(base, target, durationSeconds, actualPercent) { + var span = target - base; + if (span <= 0) { + if (actualPercent !== null && !Number.isNaN(actualPercent)) { + return Math.min(Math.max(actualPercent, base), target); + } + return base; + } + var elapsed = Math.max(0, (Date.now() - this.stageEnteredAt) / 1000); + var ratio = Math.min(1, elapsed / Math.max(durationSeconds, 1)); + var smooth = base + ratio * span; + if (actualPercent !== null && !Number.isNaN(actualPercent)) { + smooth = Math.max(smooth, Math.min(actualPercent, target)); + } + return Math.min(smooth, target); + } }, { key: "stopPolling", value: function stopPolling() {