diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
index 11958bd..9a5ae00 100644
--- a/.mokogitea/workflows/issue-branch.yml
+++ b/.mokogitea/workflows/issue-branch.yml
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
-# VERSION: 01.00.00
+# VERSION: 02.52.22
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42cc677..d8f3a35 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,20 @@
# Changelog
## [Unreleased]
-## [02.52.18] --- 2026-06-30
+### Added
+- Cancel Stalled toolbar button on Backup Records view to cancel backups stuck in "running" status
+- New ACL permission `mokosuitebackup.backup.cancel` for cancel stalled action
+- AJAX endpoint `ajax.cancelBackup` for programmatic/API cancel
+- Auto-timeout failsafe: preflight auto-cancels "running" backups older than 30 minutes
+- Pre-extension-update backup progress modal (Bootstrap 5 modal with stepped AJAX progress bar)
+
+### Fixed
+- Pre-update backup ran synchronously with no browser feedback — page hung until complete
+- Stalled backups permanently blocked future backups for the same profile
+- Preflight error message now directs users to Cancel Stalled action
## [02.52.18] --- 2026-06-30
-## [01.45.00] --- 2026-06-28
-
-
## [01.45.00] --- 2026-06-28
## [01.43.35] --- 2026-06-28
diff --git a/SECURITY.md b/SECURITY.md
index 5dfd6f1..46c440f 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
-VERSION: 02.52.18
+VERSION: 02.52.22
BRIEF: Security vulnerability reporting and handling policy
-->
diff --git a/source/packages/MokoSuiteClient b/source/packages/MokoSuiteClient
index 0a9125e..c7e6670 160000
--- a/source/packages/MokoSuiteClient
+++ b/source/packages/MokoSuiteClient
@@ -1 +1 @@
-Subproject commit 0a9125e51956a084941abccdf2de8ddd064777e8
+Subproject commit c7e66705443f74e3ee2ffdfecc08224cc40240aa
diff --git a/source/packages/com_mokosuitebackup/access.xml b/source/packages/com_mokosuitebackup/access.xml
index 37c2f9d..6f3220e 100644
--- a/source/packages/com_mokosuitebackup/access.xml
+++ b/source/packages/com_mokosuitebackup/access.xml
@@ -15,5 +15,6 @@
+
diff --git a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini
index 8547103..738fb5a 100644
--- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini
+++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini
@@ -450,6 +450,8 @@ COM_MOKOSUITEBACKUP_ACTION_BACKUP_COMPARE="Compare Backups"
COM_MOKOSUITEBACKUP_ACTION_BACKUP_COMPARE_DESC="Allows users to compare two backup records side-by-side."
COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE="Browse Archives"
COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE_DESC="Allows users to view file listings inside backup archives without extracting."
+COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup"
+COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files."
; Snapshot ACL
COM_MOKOSUITEBACKUP_ACTION_SNAPSHOT_MANAGE="Manage Snapshots"
@@ -500,6 +502,12 @@ COM_MOKOJOOMBACKUP_PURGE_INVALID_DATE="Invalid date. Please select a valid date.
COM_MOKOJOOMBACKUP_PURGE_SUCCESS="%d backup(s) purged successfully."
COM_MOKOJOOMBACKUP_PURGE_PARTIAL="%d backup(s) purged, but %d could not be deleted."
+; Cancel Stalled Backup
+COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED="Cancel Stalled"
+COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED="No backup records selected."
+COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING="None of the selected backups are in running status."
+COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled."
+
; Remote Destinations (multi-remote)
COM_MOKOJOOMBACKUP_REMOTE_DESTINATIONS="Remote Destinations"
COM_MOKOJOOMBACKUP_REMOTE_ADD="Add Destination"
diff --git a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini
index ef2e72b..1d7328d 100644
--- a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini
+++ b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini
@@ -116,3 +116,13 @@ COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND="No completed backups found before the selec
COM_MOKOJOOMBACKUP_PURGE_INVALID_DATE="Invalid date. Please select a valid date."
COM_MOKOJOOMBACKUP_PURGE_SUCCESS="%d backup(s) purged successfully."
COM_MOKOJOOMBACKUP_PURGE_PARTIAL="%d backup(s) purged, but %d could not be deleted."
+
+; Cancel Stalled Backup
+COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED="Cancel Stalled"
+COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED="No backup records selected."
+COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING="None of the selected backups are in running status."
+COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled."
+
+; ACL - Cancel
+COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup"
+COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files."
diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml
index 916c79c..6aeb44e 100644
--- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
MokoSuiteBackup
- 02.52.18
+ 02.52.22
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.20.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.20.sql
new file mode 100644
index 0000000..4812b35
--- /dev/null
+++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.20.sql
@@ -0,0 +1 @@
+/* 02.52.20 — no schema changes */
diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.21.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.21.sql
new file mode 100644
index 0000000..5727cbf
--- /dev/null
+++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.21.sql
@@ -0,0 +1 @@
+/* 02.52.21 — no schema changes */
diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.22.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.22.sql
new file mode 100644
index 0000000..dd661bd
--- /dev/null
+++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.22.sql
@@ -0,0 +1 @@
+/* 02.52.22 — no schema changes */
diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
index 159af3b..f064e0b 100644
--- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
+++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php
@@ -84,6 +84,67 @@ class AjaxController extends BaseController
$this->sendJson($result);
}
+ /**
+ * Cancel a backup record stuck in "running" status.
+ * POST: task=ajax.cancelBackup&id=123
+ */
+ public function cancelBackup(): void
+ {
+ if (!Session::checkToken('get') && !Session::checkToken('post')) {
+ $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
+
+ return;
+ }
+
+ if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) {
+ $this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
+
+ return;
+ }
+
+ $id = $this->input->getInt('id', 0);
+
+ if (!$id) {
+ $this->sendJson(['error' => true, 'message' => 'Missing record ID']);
+
+ return;
+ }
+
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select($db->quoteName(['id', 'status', 'absolute_path']))
+ ->from($db->quoteName('#__mokosuitebackup_records'))
+ ->where($db->quoteName('id') . ' = ' . $id);
+ $db->setQuery($query);
+ $record = $db->loadObject();
+
+ if (!$record) {
+ $this->sendJson(['error' => true, 'message' => 'Record not found'], 404);
+
+ return;
+ }
+
+ if ($record->status !== 'running') {
+ $this->sendJson(['error' => true, 'message' => 'Backup is not in running status']);
+
+ return;
+ }
+
+ $update = $db->getQuery(true)
+ ->update($db->quoteName('#__mokosuitebackup_records'))
+ ->set($db->quoteName('status') . ' = ' . $db->quote('fail'))
+ ->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s')))
+ ->where($db->quoteName('id') . ' = ' . $id);
+ $db->setQuery($update);
+ $db->execute();
+
+ if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
+ @unlink($record->absolute_path);
+ }
+
+ $this->sendJson(['error' => false, 'message' => 'Backup cancelled']);
+ }
+
/**
* Browse server directories for the folder picker field.
* POST: task=ajax.browseDir&path=/some/path
diff --git a/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php
index 1f09d49..b2bad20 100644
--- a/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php
+++ b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php
@@ -235,6 +235,76 @@ class BackupsController extends AdminController
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
}
+ /**
+ * Cancel selected backup records that are stuck in "running" status.
+ *
+ * Sets their status to "fail", cleans up partial archive files,
+ * and destroys any associated stepped session.
+ */
+ public function cancelStalled(): void
+ {
+ $this->checkToken();
+
+ if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) {
+ $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
+ $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
+
+ return;
+ }
+
+ $cid = $this->input->get('cid', [], 'array');
+
+ if (empty($cid)) {
+ $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED'), 'warning');
+ $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
+
+ return;
+ }
+
+ $db = $this->app->getContainer()->get('DatabaseDriver');
+ $cancelled = 0;
+ $skipped = 0;
+
+ foreach ($cid as $id) {
+ $id = (int) $id;
+
+ $query = $db->getQuery(true)
+ ->select($db->quoteName(['id', 'status', 'absolute_path']))
+ ->from($db->quoteName('#__mokosuitebackup_records'))
+ ->where($db->quoteName('id') . ' = ' . $id);
+ $db->setQuery($query);
+ $record = $db->loadObject();
+
+ if (!$record || $record->status !== 'running') {
+ $skipped++;
+
+ continue;
+ }
+
+ $update = $db->getQuery(true)
+ ->update($db->quoteName('#__mokosuitebackup_records'))
+ ->set($db->quoteName('status') . ' = ' . $db->quote('fail'))
+ ->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s')))
+ ->where($db->quoteName('id') . ' = ' . $id);
+ $db->setQuery($update);
+ $db->execute();
+
+ if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
+ @unlink($record->absolute_path);
+ }
+
+ $cancelled++;
+ }
+
+ if ($cancelled > 0) {
+ $this->setMessage(Text::sprintf('COM_MOKOJOOMBACKUP_CANCEL_SUCCESS', $cancelled));
+ } elseif ($skipped > 0) {
+ $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING'), 'warning');
+ }
+
+ $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
+ }
+
/**
* No-op target for the purge toolbar button.
*
diff --git a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php
index ac62cd3..64b42b5 100644
--- a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php
+++ b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php
@@ -194,22 +194,58 @@ class PreflightCheck
}
}
+ private const STALE_TIMEOUT_MINUTES = 30;
+
/**
* Check if another backup is already running for this profile.
+ *
+ * Backups running longer than STALE_TIMEOUT_MINUTES are automatically
+ * marked as failed so they don't permanently block future runs.
*/
private function checkRunningBackup(object $profile, object $db): void
{
$query = $db->getQuery(true)
- ->select('COUNT(*)')
+ ->select($db->quoteName(['id', 'backupstart', 'absolute_path']))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id)
->where($db->quoteName('status') . ' = ' . $db->quote('running'));
$db->setQuery($query);
- $running = (int) $db->loadResult();
+ $rows = $db->loadObjectList();
- if ($running > 0) {
+ if (empty($rows)) {
+ return;
+ }
+
+ $cutoff = time() - (self::STALE_TIMEOUT_MINUTES * 60);
+ $stillAlive = 0;
+
+ foreach ($rows as $row) {
+ $started = strtotime($row->backupstart);
+
+ if ($started !== false && $started < $cutoff) {
+ $update = $db->getQuery(true)
+ ->update($db->quoteName('#__mokosuitebackup_records'))
+ ->set($db->quoteName('status') . ' = ' . $db->quote('fail'))
+ ->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s')))
+ ->where($db->quoteName('id') . ' = ' . (int) $row->id);
+ $db->setQuery($update);
+ $db->execute();
+
+ if (!empty($row->absolute_path) && is_file($row->absolute_path)) {
+ @unlink($row->absolute_path);
+ }
+
+ $this->warnings[] = 'Auto-cancelled stalled backup #' . $row->id
+ . ' (started ' . $row->backupstart . ', exceeded '
+ . self::STALE_TIMEOUT_MINUTES . ' min timeout)';
+ } else {
+ $stillAlive++;
+ }
+ }
+
+ if ($stillAlive > 0) {
$this->errors[] = 'Another backup is already running for profile: ' . $profile->title
- . ' — wait for it to finish or delete the stale record';
+ . ' — wait for it to finish or use Cancel Stalled from the Backup Records toolbar';
}
}
diff --git a/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php b/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php
index 4816383..3b45d0f 100644
--- a/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php
+++ b/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php
@@ -113,6 +113,10 @@ class HtmlView extends BaseHtmlView
ToolbarHelper::custom('backups.compare', 'copy', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_COMPARE', true);
}
+ if ($user->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) {
+ ToolbarHelper::custom('backups.cancelStalled', 'stop-circle', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED', true);
+ }
+
if ($user->authorise('core.delete', 'com_mokosuitebackup')) {
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'backups.delete');
}
diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
index 8c6c5ba..f9cd809 100644
--- a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
+++ b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
@@ -8,7 +8,7 @@
-->
mod_mokosuitebackup_cpanel
- 02.52.18
+ 02.52.22
2026-06-23
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
index 14db151..e7759ab 100644
--- a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Action Log - MokoSuiteBackup
- 02.52.18
+ 02.52.22
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
index 1fdeb74..94003d1 100644
--- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Console - MokoSuiteBackup
- 02.52.18
+ 02.52.22
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
index 0c325b4..38e1384 100644
--- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Content - MokoSuiteBackup
- 02.52.18
+ 02.52.22
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
index d00530b..a757f43 100644
--- a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
@@ -1,7 +1,7 @@
Quick Icon - MokoSuiteBackup
- 02.52.18
+ 02.52.22
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
index 7ad71bf..0f6282c 100644
--- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
System - MokoSuiteBackup
- 02.52.18
+ 02.52.22
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
index 0a4e874..4643360 100644
--- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Task - MokoSuiteBackup
- 02.52.18
+ 02.52.22
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
index acd32a2..868f770 100644
--- a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
+++ b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
@@ -7,7 +7,7 @@
-->
Web Services - MokoSuiteBackup
- 02.52.18
+ 02.52.22
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml
index aa7a8ae..fa57eb2 100644
--- a/source/pkg_mokosuitebackup.xml
+++ b/source/pkg_mokosuitebackup.xml
@@ -8,7 +8,7 @@
Package - MokoSuiteBackup
mokosuitebackup
- 02.52.18
+ 02.52.22
2026-06-02
Moko Consulting
hello@mokoconsulting.tech