From cf191be3381b38182424f77a438104ab3d8850c5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 18 Jul 2026 15:04:10 -0500 Subject: [PATCH 1/4] fix(backup): repair Cancel Stalled fatal, stalled-run self-heal, runbackup overlay, update site - BackupsController::cancelStalled()/purge(): use Factory::getContainer() instead of the protected CMSApplication::getContainer() (fatal on Cancel Stalled) - BackupStatusHelper::markStalledAsFailed(): flip orphaned "running" records older than a 1h threshold to "fail"; swept when the records list loads - backup detail view: render "Not finished" for a null/zero backupend instead of the bogus "Monday, November 29, -0001" - runbackup: full-screen black overlay (fixed, z-indexed) instead of an in-admin-chrome card, matching the lock overlay - script.php ensureUpdateSite(): recreate a missing package update site in postflight so the "update site not found" banner self-heals Authored-by: Moko Consulting Claude-Session: https://claude.ai/code/session_019V1WPYojhKyyPHewfUsPEz --- CHANGELOG.md | 7 ++ .../language/en-GB/com_mokosuitebackup.ini | 1 + .../media/css/runbackup.css | 34 ++++++++-- .../src/Controller/BackupsController.php | 5 +- .../src/Helper/BackupStatusHelper.php | 46 +++++++++++++ .../src/View/Backups/HtmlView.php | 5 ++ .../tmpl/backup/default.php | 15 ++++- source/script.php | 66 +++++++++++++++++++ 8 files changed, 172 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7349eb83..58270954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ - Pre-update / pre-uninstall skip flag is now **time-bounded**. It records the moment it was armed and is honoured only within a short TTL (5 min), always consumed one-shot. Previously a bare boolean: if a user ran the full-screen pre-update backup then abandoned the flow (closed the tab before the update re-fired), the flag lingered for the whole session and could silently suppress a later, unrelated pre-action backup — i.e. a real update running with **no** backup. Now a stale flag is ignored and the server backs up as normal. - Pre-update / pre-uninstall backup can no longer run **twice**. The one-shot "skip the synchronous server-side backup" flag is now armed **server-side** the moment the full-screen backup completes (in `AjaxController::step`, keyed by the `msb_action` it is fronting), instead of relying solely on the best-effort client `ajax.preupdateAck` POST (whose failure was silently swallowed in a `catch()`). If that ack ever fails to land, the re-fired extension update/uninstall no longer triggers a second backup behind the "update is running" overlay. The flag is armed **only** on genuine completion (`done`, no error), so a failed or cancelled backup still lets the server back up before the action — the failure mode stays "one extra backup", never "no backup". - Component **Options** page showed the raw `COM_MOKOSUITEBACKUP_CONFIGURATION` constant instead of a translated title. Joomla's `com_config` derives the Options-page title (and the component name) from the extension *element* (`com_mokosuitebackup`), but the language files defined those keys only under the legacy `COM_MOKOJOOMBACKUP_` stem. Added the element-derived `COM_MOKOSUITEBACKUP` and `COM_MOKOSUITEBACKUP_CONFIGURATION` keys to all four language files (en-GB/en-US, `.ini` and `.sys.ini`), matching the already-migrated `COM_MOKOSUITEBACKUP_ACTION_*` ACL keys. The 400+ explicitly-referenced `COM_MOKOJOOMBACKUP_*` keys are unaffected and left as-is. (#234) +- **Cancel Stalled no longer throws a fatal error.** `BackupsController::cancelStalled()` (and `purge()`) fetched the database via `$this->app->getContainer()`, but `CMSApplication::getContainer()` is `protected` — clicking **Cancel Stalled** died with *"Call to protected method Joomla\CMS\Application\CMSApplication::getContainer() from scope ...\BackupsController"*, leaving no UI path to clear a stuck backup. Both call sites now use `Factory::getContainer()`, matching the rest of the component. +- **Interrupted backups no longer stay stuck in "running" forever.** When the driving window or CLI process dies mid-run (closed tab, PHP timeout, killed cron), the record was orphaned in `running` with no end time, showed as a phantom in-progress backup on the dashboard, and — until the fix above — could not be cleared. The records list now self-heals on load via `BackupStatusHelper::markStalledAsFailed()`, which flips `running` records older than a 1-hour threshold to `fail` (the threshold spares genuinely in-flight scheduled/CLI backups, which run without the admin's browser lock). +- **Backup detail no longer shows a bogus "Monday, November 29, -0001" end time.** An unfinished backup stores the null date `0000-00-00 00:00:00`, which `HTMLHelper::_('date', …)` rendered as a fake far-past date. The detail view now shows **Not finished** for a null/zero `backupend`. +- **Update site now self-heals.** When the package's `#__update_sites` row is missing, the records view warned *"MokoSuiteBackup update site not found. Reinstall the package to register the update server."* The install script's `postflight` now recreates the update site (and re-attaches a preserved download key) via `ensureUpdateSite()` on the next install/update — no manual reinstall required. + +### Changed +- **Run-backup progress screen is now a true full-screen black overlay.** `view=runbackup` rendered as a small Bootstrap card inside the admin chrome (sidebar/toolbar still visible); it now covers the whole viewport on black — matching the "Backup in progress" lock overlay other admin tabs show — with a centered dark panel and light-on-dark text. ## [02.65.00] --- 2026-07-16 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 a0b3f65b..73af908f 100644 --- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini @@ -123,6 +123,7 @@ COM_MOKOJOOMBACKUP_FIELD_ORIGIN="Origin" COM_MOKOJOOMBACKUP_FIELD_SIZE="Total Size" COM_MOKOJOOMBACKUP_FIELD_START="Start Time" COM_MOKOJOOMBACKUP_FIELD_END="End Time" +COM_MOKOJOOMBACKUP_NOT_FINISHED="Not finished" COM_MOKOJOOMBACKUP_FIELD_ARCHIVE="Archive Name" COM_MOKOJOOMBACKUP_FIELD_FILES_COUNT="Files Count" COM_MOKOJOOMBACKUP_FIELD_TABLES_COUNT="Tables Count" diff --git a/source/packages/com_mokosuitebackup/media/css/runbackup.css b/source/packages/com_mokosuitebackup/media/css/runbackup.css index 217c99c9..9e81b813 100644 --- a/source/packages/com_mokosuitebackup/media/css/runbackup.css +++ b/source/packages/com_mokosuitebackup/media/css/runbackup.css @@ -24,14 +24,40 @@ body { --msb-danger: var(--template-danger-color, var(--bs-danger, #c52128)); --msb-track: var(--template-bg-light, var(--bs-gray-200, #edf0f5)); - padding: 0 1rem; + /* Full-screen black overlay: cover the whole admin chrome (sidebar, toolbar) + so the run screen matches the "Backup in progress" lock overlay other admin + tabs show, instead of appearing as a small card inside the admin template. */ + position: fixed; + inset: 0; + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background-color: #000; + color: #f8f9fa; + overflow: auto; } -/* overflow:hidden clips the full-bleed bar to the card's rounded corners. - The card's gray background comes from a Bootstrap utility class on the - element itself (see the template), not a rule here. */ +/* Centered dark panel on the black backdrop. overflow:hidden clips the + full-bleed progress bar to the panel's rounded corners. */ .msb-runbackup .card { overflow: hidden; + width: 100%; + max-width: 720px; + background-color: #121417 !important; + border: 1px solid #2a2f37; + box-shadow: 0 1rem 3rem rgba(0, 0, 0, .6); +} + +/* Light-on-dark text — override Bootstrap muted greys that disappear on black. */ +.msb-runbackup .card, +.msb-runbackup .card .text-muted { + color: #f8f9fa !important; +} + +.msb-runbackup #msb-rb-subtitle { + color: #adb5bd !important; } .msb-rb-progress { diff --git a/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php index 31086528..18401285 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php @@ -12,6 +12,7 @@ namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller; defined('_JEXEC') or die; +use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\AdminController; use Joomla\CMS\Router\Route; @@ -193,7 +194,7 @@ class BackupsController extends AdminController $cutoff = $cutoffDate . ' 00:00:00'; - $db = $this->app->getContainer()->get('DatabaseDriver'); + $db = Factory::getContainer()->get('DatabaseDriver'); $query = $db->getQuery(true) ->select($db->quoteName('id')) ->from($db->quoteName('#__mokosuitebackup_records')) @@ -260,7 +261,7 @@ class BackupsController extends AdminController return; } - $db = $this->app->getContainer()->get('DatabaseDriver'); + $db = Factory::getContainer()->get('DatabaseDriver'); $cancelled = 0; $skipped = 0; diff --git a/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php b/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php index a711f975..86384dff 100644 --- a/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php +++ b/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php @@ -83,6 +83,52 @@ class BackupStatusHelper return $record ?: null; } + /** + * Mark backup records stuck in "running" as failed. + * + * The stepped engine writes status='running' with a null backupend when a run + * starts, and only flips it on completion/failure. If the driving window or CLI + * process dies mid-run (closed tab, PHP timeout, killed cron) the record is + * orphaned in "running" forever — the dashboard then shows a phantom in-progress + * backup and "recent failures" never reflects reality, and (until the cancel + * fix) it could not be cleared from the UI. + * + * A time threshold is used rather than flipping every "running" row so a + * genuinely in-flight scheduled/CLI backup — which runs without the admin's + * browser lock — is not killed prematurely. Interactive backups hold the admin + * lock, so any "running" row visible on the records list is already stale. + * + * Timestamps use date('Y-m-d H:i:s') to match exactly how the engine writes + * backupstart/backupend (avoiding a UTC-vs-server-time mismatch). + * + * @param int $timeoutSeconds Age past which a "running" record is treated as stalled (min 60). + * @return int Number of records marked failed. + */ + public static function markStalledAsFailed(int $timeoutSeconds = 3600): int + { + try { + $db = Factory::getContainer()->get('DatabaseDriver'); + + $cutoff = date('Y-m-d H:i:s', time() - max(60, $timeoutSeconds)); + $now = date('Y-m-d H:i:s'); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitebackup_records')) + ->set($db->quoteName('status') . ' = ' . $db->quote('fail')) + ->set($db->quoteName('backupend') . ' = ' . $db->quote($now)) + ->where($db->quoteName('status') . ' = ' . $db->quote('running')) + ->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)); + + $db->setQuery($query); + $db->execute(); + + return (int) $db->getAffectedRows(); + } catch (\Throwable $e) { + // Non-critical housekeeping — never break the page over it. + return 0; + } + } + /** * Get a full status summary for heartbeat payloads. * diff --git a/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php b/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php index ba43de36..8d452b22 100644 --- a/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php +++ b/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php @@ -17,6 +17,7 @@ use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; use Joomla\CMS\Router\Route; use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Component\MokoSuiteBackup\Administrator\Helper\BackupStatusHelper; class HtmlView extends BaseHtmlView { @@ -28,6 +29,10 @@ class HtmlView extends BaseHtmlView public function display($tpl = null): void { + // Self-heal any backups orphaned in "running" by a died window/CLI before + // listing, so the records list never shows a phantom in-progress backup. + BackupStatusHelper::markStalledAsFailed(); + $this->items = $this->get('Items'); $this->pagination = $this->get('Pagination'); $this->state = $this->get('State'); diff --git a/source/packages/com_mokosuitebackup/tmpl/backup/default.php b/source/packages/com_mokosuitebackup/tmpl/backup/default.php index 49dfd589..f6a8f820 100644 --- a/source/packages/com_mokosuitebackup/tmpl/backup/default.php +++ b/source/packages/com_mokosuitebackup/tmpl/backup/default.php @@ -62,7 +62,20 @@ $ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false) - item->backupend, Text::_('DATE_FORMAT_LC2')); ?> + + item->backupend; + + if ($backupEnd === '' || strncmp($backupEnd, '0000-00-00', 10) === 0) { + echo '' . $this->escape(Text::_('COM_MOKOJOOMBACKUP_NOT_FINISHED')) . ''; + } else { + echo HTMLHelper::_('date', $backupEnd, Text::_('DATE_FORMAT_LC2')); + } + ?> + diff --git a/source/script.php b/source/script.php index 79d9dfe0..6688c84f 100644 --- a/source/script.php +++ b/source/script.php @@ -152,6 +152,11 @@ class Pkg_MokoSuiteBackupInstallerScript return; } + /* Self-heal the package update site if it is missing (the records view warns + "update site not found" otherwise). Runs before restoreDownloadKey so a + preserved download key can re-attach to the recreated site. */ + $this->ensureUpdateSite(); + /* Restore the download key preserved before the update re-registered the site */ if ($type === 'update') { $this->restoreDownloadKey(); @@ -1068,6 +1073,67 @@ class Pkg_MokoSuiteBackupInstallerScript } } + /** + * Ensure the package has an update site registered. + * + * Joomla normally creates this row from the manifest on install, + * but if it is ever missing (partial install, manual DB surgery, a package that + * predates the update-server tag) the records view warns "update site not found" + * and the extension can never pull updates. Recreate it idempotently so the + * install is self-healing — no manual reinstall required. + */ + private function ensureUpdateSite(): void + { + $updateUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/updates.xml'; + + try { + $db = Factory::getDbo(); + + /* Resolve the package extension id. */ + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokosuitebackup')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')) + ->setLimit(1); + $extensionId = (int) $db->setQuery($query)->loadResult(); + + if ($extensionId === 0) { + return; + } + + /* Already linked to an update site? Nothing to do. */ + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__update_sites_extensions')) + ->where($db->quoteName('extension_id') . ' = ' . $extensionId); + + if ((int) $db->setQuery($query)->loadResult() > 0) { + return; + } + + /* Create the update site and link it to the package, preserving any + download key we saved earlier in this run. */ + $site = (object) [ + 'name' => 'MokoSuiteBackup Updates', + 'type' => 'extension', + 'location' => $updateUrl, + 'enabled' => 1, + 'last_check_timestamp' => 0, + 'extra_query' => !empty($this->savedDownloadKey) ? 'dlid=' . $this->savedDownloadKey : '', + ]; + $db->insertObject('#__update_sites', $site, 'update_site_id'); + + $link = (object) [ + 'update_site_id' => (int) $site->update_site_id, + 'extension_id' => $extensionId, + ]; + $db->insertObject('#__update_sites_extensions', $link); + } catch (\Throwable $e) { + error_log('MokoSuiteBackup: ensureUpdateSite() failed: ' . $e->getMessage()); + } + } + /** * Show post-install license key prompt. */ -- 2.52.0 From 36098fa606d4ec73d2bd9e3621d3e857ca26a924 Mon Sep 17 00:00:00 2001 From: "mokogit-actions[bot]" Date: Sat, 18 Jul 2026 20:08:17 +0000 Subject: [PATCH 2/4] chore(version): pre-release bump to 02.67.04-dev [skip ci] --- .mokogit/workflows/issue-branch.yml | 2 +- SECURITY.md | 2 +- source/packages/MokoSuiteClient | 2 +- source/packages/com_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/com_mokosuitebackup/sql/updates/mysql/02.67.04.sql | 1 + .../mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml | 2 +- .../packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml | 2 +- .../plg_webservices_mokosuitebackup/mokosuitebackup.xml | 2 +- source/pkg_mokosuitebackup.xml | 2 +- 14 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 source/packages/com_mokosuitebackup/sql/updates/mysql/02.67.04.sql diff --git a/.mokogit/workflows/issue-branch.yml b/.mokogit/workflows/issue-branch.yml index 7f2d9c5d..62e6e553 100644 --- a/.mokogit/workflows/issue-branch.yml +++ b/.mokogit/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Automation -# VERSION: 02.66.05 +# VERSION: 02.67.04 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/SECURITY.md b/SECURITY.md index cfd05383..2499962e 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.66.05 +VERSION: 02.67.04 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/MokoSuiteClient b/source/packages/MokoSuiteClient index 6bb4f884..252e7779 160000 --- a/source/packages/MokoSuiteClient +++ b/source/packages/MokoSuiteClient @@ -1 +1 @@ -Subproject commit 6bb4f884d44cc0e3ff8e2d3c4691e8273bc6a1cf +Subproject commit 252e7779b24ae7bf392c201921a92712d7238db4 diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index 4043eefd..7ee9a7d5 100644 --- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml @@ -18,7 +18,7 @@ --> com_mokosuitebackup Component - MokoSuiteBackup - 02.66.05 + 02.67.04 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.67.04.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.67.04.sql new file mode 100644 index 00000000..d714fb38 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.67.04.sql @@ -0,0 +1 @@ +/* 02.67.04 — no schema changes */ diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml index a5028039..be2385e3 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 @@ --> Module - MokoSuiteBackup - cPanel - 02.66.05 + 02.67.04 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 046b6845..df50f6f6 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.66.05 + 02.67.04 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 75b3cd61..6dd36500 100644 --- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Console - MokoSuiteBackup - 02.66.05 + 02.67.04 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 9fe2196d..8bbaa97a 100644 --- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Content - MokoSuiteBackup - 02.66.05 + 02.67.04 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 792aaf91..d3f676d4 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.66.05 + 02.67.04 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 adfd5fdf..087ef375 100644 --- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> System - MokoSuiteBackup - 02.66.05 + 02.67.04 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 cae45280..b73b4e69 100644 --- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Task - MokoSuiteBackup - 02.66.05 + 02.67.04 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 2dddd97a..ba6820df 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.66.05 + 02.67.04 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml index 6a50b54d..c0276203 100644 --- a/source/pkg_mokosuitebackup.xml +++ b/source/pkg_mokosuitebackup.xml @@ -8,7 +8,7 @@ Package - MokoSuiteBackup mokosuitebackup - 02.66.05 + 02.67.04 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 93b7437e0c0e62be76b2a9ffd0f9cf6ad7207496 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 18 Jul 2026 15:27:12 -0500 Subject: [PATCH 3/4] =?UTF-8?q?fix(backup):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20transactional=20update=20site,=20safer=20stalled=20?= =?UTF-8?q?sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Silent-failure-hunter: - ensureUpdateSite(): wrap the two inserts in a transaction so a failed link insert cannot orphan a #__update_sites row (the idempotency guard checks only the link table, so an orphan would be re-created on every retry). Surface failures via Log::add(jerror) + enqueueMessage instead of a silent error_log(), matching restoreDownloadKey(). Code-reviewer: - markStalledAsFailed(): raise threshold to a generous 6h (well above PreflightCheck's 30-min per-profile start guard) so a long browser/CLI backup is not reaped mid-flight; delete partial archives for reaped rows to match PreflightCheck; add a WARNING breadcrumb in the catch. Real fix (heartbeat column) tracked as a follow-up in the stalled-backup issue. Authored-by: Moko Consulting Claude-Session: https://claude.ai/code/session_019V1WPYojhKyyPHewfUsPEz --- CHANGELOG.md | 2 +- .../src/Helper/BackupStatusHelper.php | 69 ++++++++++++++----- source/script.php | 56 ++++++++++----- 3 files changed, 92 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58270954..93987359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Pre-update / pre-uninstall backup can no longer run **twice**. The one-shot "skip the synchronous server-side backup" flag is now armed **server-side** the moment the full-screen backup completes (in `AjaxController::step`, keyed by the `msb_action` it is fronting), instead of relying solely on the best-effort client `ajax.preupdateAck` POST (whose failure was silently swallowed in a `catch()`). If that ack ever fails to land, the re-fired extension update/uninstall no longer triggers a second backup behind the "update is running" overlay. The flag is armed **only** on genuine completion (`done`, no error), so a failed or cancelled backup still lets the server back up before the action — the failure mode stays "one extra backup", never "no backup". - Component **Options** page showed the raw `COM_MOKOSUITEBACKUP_CONFIGURATION` constant instead of a translated title. Joomla's `com_config` derives the Options-page title (and the component name) from the extension *element* (`com_mokosuitebackup`), but the language files defined those keys only under the legacy `COM_MOKOJOOMBACKUP_` stem. Added the element-derived `COM_MOKOSUITEBACKUP` and `COM_MOKOSUITEBACKUP_CONFIGURATION` keys to all four language files (en-GB/en-US, `.ini` and `.sys.ini`), matching the already-migrated `COM_MOKOSUITEBACKUP_ACTION_*` ACL keys. The 400+ explicitly-referenced `COM_MOKOJOOMBACKUP_*` keys are unaffected and left as-is. (#234) - **Cancel Stalled no longer throws a fatal error.** `BackupsController::cancelStalled()` (and `purge()`) fetched the database via `$this->app->getContainer()`, but `CMSApplication::getContainer()` is `protected` — clicking **Cancel Stalled** died with *"Call to protected method Joomla\CMS\Application\CMSApplication::getContainer() from scope ...\BackupsController"*, leaving no UI path to clear a stuck backup. Both call sites now use `Factory::getContainer()`, matching the rest of the component. -- **Interrupted backups no longer stay stuck in "running" forever.** When the driving window or CLI process dies mid-run (closed tab, PHP timeout, killed cron), the record was orphaned in `running` with no end time, showed as a phantom in-progress backup on the dashboard, and — until the fix above — could not be cleared. The records list now self-heals on load via `BackupStatusHelper::markStalledAsFailed()`, which flips `running` records older than a 1-hour threshold to `fail` (the threshold spares genuinely in-flight scheduled/CLI backups, which run without the admin's browser lock). +- **Interrupted backups no longer stay stuck in "running" forever.** When the driving window or CLI process dies mid-run (closed tab, PHP timeout, killed cron), the record was orphaned in `running` with no end time, showed as a phantom in-progress backup on the dashboard, and — until the fix above — could not be cleared. The records list now self-heals on load via `BackupStatusHelper::markStalledAsFailed()`, which flips `running` records older than a generous 6-hour threshold to `fail` and deletes their partial archives (the threshold sits well above `PreflightCheck`'s 30-minute per-profile start guard so a genuinely long-running browser/CLI backup is never reaped mid-flight; a per-step heartbeat column is the real fix, tracked as a follow-up). - **Backup detail no longer shows a bogus "Monday, November 29, -0001" end time.** An unfinished backup stores the null date `0000-00-00 00:00:00`, which `HTMLHelper::_('date', …)` rendered as a fake far-past date. The detail view now shows **Not finished** for a null/zero `backupend`. - **Update site now self-heals.** When the package's `#__update_sites` row is missing, the records view warned *"MokoSuiteBackup update site not found. Reinstall the package to register the update server."* The install script's `postflight` now recreates the update site (and re-attaches a preserved download key) via `ensureUpdateSite()` on the next install/update — no manual reinstall required. diff --git a/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php b/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php index 86384dff..00763831 100644 --- a/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php +++ b/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php @@ -13,6 +13,7 @@ namespace Joomla\Component\MokoSuiteBackup\Administrator\Helper; defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\Log\Log; /** * Lightweight helper for external consumers (bridge plugins, MCP servers, CLI tools) @@ -84,7 +85,20 @@ class BackupStatusHelper } /** - * Mark backup records stuck in "running" as failed. + * Seconds a record may sit in "running" before the records-list sweep treats it + * as stalled. Deliberately far more generous than + * PreflightCheck::STALE_TIMEOUT_MINUTES (30 min): that guard reaps per-profile at + * backup *start* (and blocks a concurrent run), whereas this sweep runs on every + * records-list load and must never reap a genuinely long-running browser/CLI + * backup mid-flight. Without a per-step heartbeat column, age can only be measured + * from backupstart — a heartbeat column is the real fix (tracked in the + * stalled-backup issue). + */ + private const STALLED_TIMEOUT_SECONDS = 21600; + + /** + * Mark backup records stuck in "running" as failed and clean up their partial + * archives. * * The stepped engine writes status='running' with a null backupend when a run * starts, and only flips it on completion/failure. If the driving window or CLI @@ -93,38 +107,57 @@ class BackupStatusHelper * backup and "recent failures" never reflects reality, and (until the cancel * fix) it could not be cleared from the UI. * - * A time threshold is used rather than flipping every "running" row so a - * genuinely in-flight scheduled/CLI backup — which runs without the admin's - * browser lock — is not killed prematurely. Interactive backups hold the admin - * lock, so any "running" row visible on the records list is already stale. - * - * Timestamps use date('Y-m-d H:i:s') to match exactly how the engine writes + * This complements PreflightCheck::checkRunningBackup(), which reaps per-profile + * at backup start; here we clear truly-dead phantom rows shown on the records + * list. A deliberately generous threshold is used so an in-flight scheduled/CLI + * backup (which runs without the admin's browser lock) is not killed prematurely. + * Timestamps use date('Y-m-d H:i:s') to match how the engine writes * backupstart/backupend (avoiding a UTC-vs-server-time mismatch). * * @param int $timeoutSeconds Age past which a "running" record is treated as stalled (min 60). * @return int Number of records marked failed. */ - public static function markStalledAsFailed(int $timeoutSeconds = 3600): int + public static function markStalledAsFailed(int $timeoutSeconds = self::STALLED_TIMEOUT_SECONDS): int { try { - $db = Factory::getContainer()->get('DatabaseDriver'); - + $db = Factory::getContainer()->get('DatabaseDriver'); $cutoff = date('Y-m-d H:i:s', time() - max(60, $timeoutSeconds)); - $now = date('Y-m-d H:i:s'); + // Select stalled rows first so we can also delete their partial archive + // files, matching PreflightCheck::checkRunningBackup()'s cleanup. $query = $db->getQuery(true) - ->update($db->quoteName('#__mokosuitebackup_records')) - ->set($db->quoteName('status') . ' = ' . $db->quote('fail')) - ->set($db->quoteName('backupend') . ' = ' . $db->quote($now)) + ->select($db->quoteName(['id', 'absolute_path'])) + ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('status') . ' = ' . $db->quote('running')) ->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)); + $rows = $db->setQuery($query)->loadObjectList(); - $db->setQuery($query); - $db->execute(); + if (empty($rows)) { + return 0; + } - return (int) $db->getAffectedRows(); + $now = date('Y-m-d H:i:s'); + + foreach ($rows as $row) { + $update = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitebackup_records')) + ->set($db->quoteName('status') . ' = ' . $db->quote('fail')) + ->set($db->quoteName('backupend') . ' = ' . $db->quote($now)) + ->where($db->quoteName('id') . ' = ' . (int) $row->id); + $db->setQuery($update)->execute(); + + if (!empty($row->absolute_path) && is_file($row->absolute_path)) { + @unlink($row->absolute_path); + } + } + + return \count($rows); } catch (\Throwable $e) { - // Non-critical housekeeping — never break the page over it. + // Non-critical housekeeping — never break the records list over it, but + // leave a breadcrumb so a persistent failure (e.g. a missing column after + // a botched migration) is diagnosable instead of silently recurring. + Log::add('MokoSuiteBackup: markStalledAsFailed() failed: ' . $e->getMessage(), Log::WARNING, 'jerror'); + return 0; } } diff --git a/source/script.php b/source/script.php index 6688c84f..1883812c 100644 --- a/source/script.php +++ b/source/script.php @@ -1113,24 +1113,48 @@ class Pkg_MokoSuiteBackupInstallerScript } /* Create the update site and link it to the package, preserving any - download key we saved earlier in this run. */ - $site = (object) [ - 'name' => 'MokoSuiteBackup Updates', - 'type' => 'extension', - 'location' => $updateUrl, - 'enabled' => 1, - 'last_check_timestamp' => 0, - 'extra_query' => !empty($this->savedDownloadKey) ? 'dlid=' . $this->savedDownloadKey : '', - ]; - $db->insertObject('#__update_sites', $site, 'update_site_id'); + download key we saved earlier in this run. Both inserts run in a + transaction: the idempotency guard above checks only the LINK table, so + a site row left behind by a failed link insert would be re-created on + every retry — accumulating orphaned duplicates. All-or-nothing avoids that. */ + $db->transactionStart(); - $link = (object) [ - 'update_site_id' => (int) $site->update_site_id, - 'extension_id' => $extensionId, - ]; - $db->insertObject('#__update_sites_extensions', $link); + try { + $site = (object) [ + 'name' => 'MokoSuiteBackup Updates', + 'type' => 'extension', + 'location' => $updateUrl, + 'enabled' => 1, + 'last_check_timestamp' => 0, + 'extra_query' => !empty($this->savedDownloadKey) ? 'dlid=' . $this->savedDownloadKey : '', + ]; + $db->insertObject('#__update_sites', $site, 'update_site_id'); + + $link = (object) [ + 'update_site_id' => (int) $site->update_site_id, + 'extension_id' => $extensionId, + ]; + $db->insertObject('#__update_sites_extensions', $link); + + $db->transactionCommit(); + } catch (\Throwable $e) { + $db->transactionRollback(); + + throw $e; + } } catch (\Throwable $e) { - error_log('MokoSuiteBackup: ensureUpdateSite() failed: ' . $e->getMessage()); + /* Surface the failure like restoreDownloadKey() rather than hiding it in + the server log: this runs at install/update time (an observed moment) + and a silent failure leaves the admin in the exact "update site not + found" state this method exists to fix, with no reason given. */ + Log::add('MokoSuiteBackup: ensureUpdateSite() failed: ' . $e->getMessage(), Log::WARNING, 'jerror'); + + Factory::getApplication()->enqueueMessage( + 'MokoSuiteBackup — the update site could not be registered automatically, ' + . 'so update notifications may not appear. You can add it manually in the ' + . 'Update Sites manager.', + 'warning' + ); } } -- 2.52.0 From 166444b845e0af143a71f20d485bfb5f5f54bfae Mon Sep 17 00:00:00 2001 From: "mokogit-actions[bot]" Date: Sat, 18 Jul 2026 20:30:00 +0000 Subject: [PATCH 4/4] chore(version): pre-release bump to 02.67.05-dev [skip ci] --- .mokogit/workflows/issue-branch.yml | 2 +- SECURITY.md | 2 +- source/packages/com_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/com_mokosuitebackup/sql/updates/mysql/02.67.05.sql | 1 + .../mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml | 2 +- .../packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml | 2 +- .../packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml | 2 +- source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml | 2 +- .../plg_webservices_mokosuitebackup/mokosuitebackup.xml | 2 +- source/pkg_mokosuitebackup.xml | 2 +- 13 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 source/packages/com_mokosuitebackup/sql/updates/mysql/02.67.05.sql diff --git a/.mokogit/workflows/issue-branch.yml b/.mokogit/workflows/issue-branch.yml index 62e6e553..550dca48 100644 --- a/.mokogit/workflows/issue-branch.yml +++ b/.mokogit/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Automation -# VERSION: 02.67.04 +# VERSION: 02.67.05 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/SECURITY.md b/SECURITY.md index 2499962e..789c4463 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.67.04 +VERSION: 02.67.05 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index 7ee9a7d5..5d238fae 100644 --- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml @@ -18,7 +18,7 @@ --> com_mokosuitebackup Component - MokoSuiteBackup - 02.67.04 + 02.67.05 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.67.05.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.67.05.sql new file mode 100644 index 00000000..0682ba61 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.67.05.sql @@ -0,0 +1 @@ +/* 02.67.05 — no schema changes */ diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml index be2385e3..9fd474fc 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 @@ --> Module - MokoSuiteBackup - cPanel - 02.67.04 + 02.67.05 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 df50f6f6..fc9ee9dd 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.67.04 + 02.67.05 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 6dd36500..456ce4e3 100644 --- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Console - MokoSuiteBackup - 02.67.04 + 02.67.05 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 8bbaa97a..5f34b7ab 100644 --- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Content - MokoSuiteBackup - 02.67.04 + 02.67.05 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 d3f676d4..f913bcd2 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.67.04 + 02.67.05 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 087ef375..d8f2ef98 100644 --- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> System - MokoSuiteBackup - 02.67.04 + 02.67.05 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 b73b4e69..799eaed1 100644 --- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Task - MokoSuiteBackup - 02.67.04 + 02.67.05 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 ba6820df..e60b957b 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.67.04 + 02.67.05 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml index c0276203..c51d549a 100644 --- a/source/pkg_mokosuitebackup.xml +++ b/source/pkg_mokosuitebackup.xml @@ -8,7 +8,7 @@ Package - MokoSuiteBackup mokosuitebackup - 02.67.04 + 02.67.05 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0