diff --git a/.mokogit/workflows/issue-branch.yml b/.mokogit/workflows/issue-branch.yml
index 213570d9..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.02
+# VERSION: 02.67.05
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe4badbc..98359bdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,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 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.
+
+### 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.66.00] --- 2026-07-16
diff --git a/SECURITY.md b/SECURITY.md
index 0ec75ef0..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.02
+VERSION: 02.67.05
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/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/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml
index a2341443..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.02
+ 02.67.05
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/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/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..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)
@@ -83,6 +84,84 @@ class BackupStatusHelper
return $record ?: null;
}
+ /**
+ * 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
+ * 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.
+ *
+ * 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 = self::STALLED_TIMEOUT_SECONDS): int
+ {
+ try {
+ $db = Factory::getContainer()->get('DatabaseDriver');
+ $cutoff = date('Y-m-d H:i:s', time() - max(60, $timeoutSeconds));
+
+ // Select stalled rows first so we can also delete their partial archive
+ // files, matching PreflightCheck::checkRunningBackup()'s cleanup.
+ $query = $db->getQuery(true)
+ ->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();
+
+ if (empty($rows)) {
+ return 0;
+ }
+
+ $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 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;
+ }
+ }
+
/**
* 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/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
index e5dddb2b..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.02
+ 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 836bb756..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.02
+ 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 d3397064..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.02
+ 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 0a0240e0..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.02
+ 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 7088c64c..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.02
+ 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 47162bc3..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.02
+ 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 0576967f..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.02
+ 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 a82682c0..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.02
+ 02.67.05
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml
index d58184f4..c51d549a 100644
--- a/source/pkg_mokosuitebackup.xml
+++ b/source/pkg_mokosuitebackup.xml
@@ -8,7 +8,7 @@
Package - MokoSuiteBackup
mokosuitebackup
- 02.67.02
+ 02.67.05
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/script.php b/source/script.php
index 79d9dfe0..1883812c 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,91 @@ 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. 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();
+
+ 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) {
+ /* 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'
+ );
+ }
+ }
+
/**
* Show post-install license key prompt.
*/