diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb620142..0733ee39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
## [Unreleased]
### Added
+- Pre-update backup **notice + live-progress modal** on the admin update pages (Joomla Update and Extensions → Update). Because the server-side `onExtensionBeforeUpdate` backup runs synchronously and can't drive a browser modal, the system plugin now injects a "back up before you update" notice with a **Back up now** button that runs the same stepped backup as the dashboard and shows a live progress bar. On success it pings a new `ajax.preupdateAck` endpoint, which arms the same throttle the server-side hook checks — so clicking Joomla's Update afterwards won't run a duplicate backup. Gated by the existing `show_update_notice` + `backup_before_update` params. (Auto-intercepting the Update button is the planned phase-2 follow-up — see #196.) (#196)
- Retention now prunes **remote** copies too: when a backup is pruned by age/count, its archive is deleted from every enabled remote destination (SFTP / FTP / S3 / Google Drive), not just the local copy. Each uploader gained an idempotent `delete()` method (already-absent file = success), and removal is best-effort — a failing destination is logged but never blocks local pruning. The shared standalone `restore.php` is intentionally left in place (every backup overwrites it, so newer backups still depend on it). (#229)
### Changed
diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
index 9d568ad7..cda1f601 100644
--- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
+++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
@@ -84,6 +84,35 @@ class AjaxController extends BaseController
$this->sendJson($result);
}
+ /**
+ * Mark the pre-update backup as satisfied for this session.
+ *
+ * Called by the update-page modal after a successful on-demand backup so the
+ * imminent server-side onExtensionBeforeUpdate backup is skipped — it sets
+ * the same 10-minute throttle key the system plugin checks, preventing a
+ * duplicate backup when the admin then clicks Joomla's Update button.
+ * POST: task=ajax.preupdateAck
+ */
+ public function preupdateAck(): void
+ {
+ if (!Session::checkToken('get') && !Session::checkToken('post')) {
+ $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
+
+ return;
+ }
+
+ if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
+ $this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
+
+ return;
+ }
+
+ // Same key + semantics as plg_system_mokosuitebackup::runPreActionBackup().
+ Factory::getSession()->set('mokosuitebackup.preaction_backup_before_update', time());
+
+ $this->sendJson(['error' => false, 'message' => 'Pre-update backup acknowledged']);
+ }
+
/**
* Cancel a backup record stuck in "running" status.
* POST: task=ajax.cancelBackup&id=123
diff --git a/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini b/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini
index d0804cd2..a465eb30 100644
--- a/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini
+++ b/source/packages/plg_system_mokosuitebackup/language/en-GB/plg_system_mokosuitebackup.ini
@@ -7,3 +7,10 @@ PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_AGE="Max Backup Age (days)"
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_AGE_DESC="Delete backup records older than this many days."
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_BACKUPS="Max Backup Count"
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_BACKUPS_DESC="Keep at most this many completed backup records."
+PLG_SYSTEM_MOKOJOOMBACKUP_UPDATE_NOTICE="MokoSuiteBackup: a full-site backup is recommended before you update."
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_NOW="Back up now"
+PLG_SYSTEM_MOKOJOOMBACKUP_DISMISS="Dismiss"
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKING_UP="Backing up…"
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update."
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed"
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…"
diff --git a/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini b/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini
index 15103fbb..03a35287 100644
--- a/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini
+++ b/source/packages/plg_system_mokosuitebackup/language/en-US/plg_system_mokosuitebackup.ini
@@ -7,3 +7,10 @@ PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_AGE="Max Backup Age (days)"
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_AGE_DESC="Delete backup records older than this many days."
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_BACKUPS="Max Backup Count"
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_BACKUPS_DESC="Keep at most this many completed backup records."
+PLG_SYSTEM_MOKOJOOMBACKUP_UPDATE_NOTICE="MokoSuiteBackup: a full-site backup is recommended before you update."
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_NOW="Back up now"
+PLG_SYSTEM_MOKOJOOMBACKUP_DISMISS="Dismiss"
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKING_UP="Backing up…"
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update."
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed"
+PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…"
diff --git a/source/packages/plg_system_mokosuitebackup/media/js/update-backup.js b/source/packages/plg_system_mokosuitebackup/media/js/update-backup.js
new file mode 100644
index 00000000..493b45ea
--- /dev/null
+++ b/source/packages/plg_system_mokosuitebackup/media/js/update-backup.js
@@ -0,0 +1,202 @@
+/**
+ * MokoSuiteBackup — pre-update backup notice + live-progress modal.
+ *
+ * Injected by plg_system_mokosuitebackup on the Joomla core-update
+ * (com_joomlaupdate) and extensions-update (com_installer&view=update) pages.
+ *
+ * Why a client-side modal: the server-side onExtensionBeforeUpdate backup runs
+ * synchronously mid-request and cannot drive a browser modal. This offers an
+ * on-demand "Back up now" button that runs the same stepped backup the
+ * dashboard uses (ajax.init then a loop of ajax.step) and shows live progress.
+ * On success it pings ajax.preupdateAck so the imminent server-side pre-update
+ * backup is skipped (no duplicate backup) when the admin clicks Update.
+ *
+ * Config comes from Joomla.getOptions('plg_system_mokosuitebackup.preupdate').
+ */
+(function () {
+ 'use strict';
+
+ var cfg = (window.Joomla && Joomla.getOptions)
+ ? Joomla.getOptions('plg_system_mokosuitebackup.preupdate', null)
+ : null;
+
+ if (!cfg || !cfg.ajaxUrl || !cfg.token) {
+ return;
+ }
+
+ var L = cfg.labels || {};
+ var running = false;
+
+ document.addEventListener('DOMContentLoaded', function () {
+ injectNotice();
+ });
+
+ function injectNotice() {
+ var host = document.querySelector('main') || document.querySelector('#content') || document.body;
+
+ if (!host || document.getElementById('msb-preupdate-notice')) {
+ return;
+ }
+
+ var bar = document.createElement('div');
+ bar.id = 'msb-preupdate-notice';
+ bar.setAttribute('role', 'status');
+ bar.style.cssText = 'display:flex;align-items:center;gap:1rem;flex-wrap:wrap;'
+ + 'margin:0 0 1rem 0;padding:.75rem 1rem;border:1px solid #f0c36d;'
+ + 'background:#fcf8e3;border-radius:.35rem;color:#8a6d3b;font-size:.95rem;';
+
+ var text = document.createElement('span');
+ text.style.cssText = 'flex:1 1 auto;';
+ text.textContent = L.notice || 'A full-site backup is recommended before you update.';
+
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.id = 'msb-preupdate-run';
+ btn.className = 'btn btn-warning btn-sm';
+ btn.textContent = L.backupNow || 'Back up now';
+ btn.addEventListener('click', runBackup);
+
+ var dismiss = document.createElement('button');
+ dismiss.type = 'button';
+ dismiss.className = 'btn btn-link btn-sm';
+ dismiss.style.cssText = 'color:#8a6d3b;';
+ dismiss.textContent = L.dismiss || 'Dismiss';
+ dismiss.addEventListener('click', function () { bar.remove(); });
+
+ bar.appendChild(text);
+ bar.appendChild(btn);
+ bar.appendChild(dismiss);
+ host.insertBefore(bar, host.firstChild);
+ }
+
+ /* ── The live-progress overlay (dependency-free, mirrors the dashboard) ── */
+
+ function buildOverlay() {
+ var overlay = document.createElement('div');
+ overlay.id = 'msb-preupdate-overlay';
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:10000;display:flex;'
+ + 'align-items:center;justify-content:center;background:rgba(0,0,0,.5);';
+
+ var box = document.createElement('div');
+ box.style.cssText = 'width:min(480px,92vw);background:#fff;border-radius:.5rem;'
+ + 'padding:1.5rem;box-shadow:0 10px 40px rgba(0,0,0,.3);';
+
+ var title = document.createElement('h3');
+ title.id = 'msb-pu-title';
+ title.style.cssText = 'margin:0 0 1rem 0;font-size:1.15rem;';
+ title.textContent = L.backingUp || 'Backing up…';
+
+ var track = document.createElement('div');
+ track.style.cssText = 'height:1.25rem;background:#e9ecef;border-radius:.35rem;overflow:hidden;';
+
+ var barfill = document.createElement('div');
+ barfill.id = 'msb-pu-bar';
+ barfill.style.cssText = 'height:100%;width:0;background:#198754;transition:width .3s ease;';
+ track.appendChild(barfill);
+
+ var status = document.createElement('div');
+ status.id = 'msb-pu-status';
+ status.style.cssText = 'margin-top:.75rem;font-size:.9rem;color:#555;';
+ status.textContent = L.starting || 'Starting backup…';
+
+ box.appendChild(title);
+ box.appendChild(track);
+ box.appendChild(status);
+ overlay.appendChild(box);
+ document.body.appendChild(overlay);
+
+ return { overlay: overlay, title: title, bar: barfill, status: status };
+ }
+
+ function setProgress(ui, progress, message) {
+ var pct = Math.max(0, Math.min(100, parseInt(progress, 10) || 0));
+ ui.bar.style.width = pct + '%';
+
+ if (message) {
+ ui.status.textContent = message;
+ }
+ }
+
+ function post(params) {
+ var body = new URLSearchParams();
+ body.append(cfg.token, '1');
+
+ Object.keys(params).forEach(function (k) {
+ body.append(k, params[k]);
+ });
+
+ return fetch(cfg.ajaxUrl, {
+ method: 'POST',
+ body: body,
+ headers: { 'X-Requested-With': 'XMLHttpRequest' }
+ }).then(function (r) { return r.json(); });
+ }
+
+ async function runBackup() {
+ if (running) {
+ return;
+ }
+
+ running = true;
+
+ var runBtn = document.getElementById('msb-preupdate-run');
+
+ if (runBtn) {
+ runBtn.disabled = true;
+ }
+
+ var ui = buildOverlay();
+ var guard = function (e) { e.preventDefault(); e.returnValue = ''; };
+ window.addEventListener('beforeunload', guard);
+
+ try {
+ var init = await post({ task: 'ajax.init', profile_id: cfg.profileId });
+
+ if (!init || init.error || !init.session_id) {
+ throw new Error((init && init.message) || 'Could not start backup');
+ }
+
+ setProgress(ui, init.progress, init.message);
+
+ var done = false;
+
+ while (!done) {
+ var step = await post({ task: 'ajax.step', session_id: init.session_id });
+
+ if (!step || step.error) {
+ throw new Error((step && step.message) || 'Backup step failed');
+ }
+
+ setProgress(ui, step.progress, step.message);
+ done = step.done || false;
+ }
+
+ /* Tell the server the pre-update backup is satisfied so the
+ synchronous onExtensionBeforeUpdate backup is skipped. */
+ try { await post({ task: 'ajax.preupdateAck' }); } catch (ignore) {}
+
+ ui.title.textContent = L.complete || 'Backup complete — safe to update.';
+ setProgress(ui, 100, '');
+
+ var notice = document.getElementById('msb-preupdate-notice');
+
+ if (notice) {
+ notice.style.background = '#d1e7dd';
+ notice.style.borderColor = '#badbcc';
+ notice.style.color = '#0f5132';
+ }
+
+ setTimeout(function () { ui.overlay.remove(); }, 1500);
+ } catch (err) {
+ ui.title.textContent = (L.failed || 'Backup failed') + ': ' + err.message;
+ ui.bar.style.background = '#dc3545';
+
+ if (runBtn) {
+ runBtn.disabled = false;
+ }
+ } finally {
+ running = false;
+ window.removeEventListener('beforeunload', guard);
+ }
+ }
+})();
diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
index ad232c6c..2aa8e82d 100644
--- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
@@ -29,6 +29,10 @@
language/en-GB/plg_system_mokosuitebackup.sys.ini
+
+ js
+
+