diff --git a/source/packages/plg_system_mokosuiteclient_backup/language/en-GB/plg_system_mokosuiteclient_backup.ini b/source/packages/plg_system_mokosuiteclient_backup/language/en-GB/plg_system_mokosuiteclient_backup.ini
new file mode 100644
index 00000000..fc4e6e46
--- /dev/null
+++ b/source/packages/plg_system_mokosuiteclient_backup/language/en-GB/plg_system_mokosuiteclient_backup.ini
@@ -0,0 +1,13 @@
+; MokoSuiteClient Backup Bridge Plugin
+; Copyright (C) 2026 Moko Consulting. All rights reserved.
+; License: GPL-3.0-or-later
+
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP="System - MokoSuiteClient Backup"
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC="Detects MokoSuiteBackup and includes backup status in heartbeat payloads sent to MokoSuiteHQ."
+
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_FIELDSET_BASIC="Backup Monitoring"
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_FIELDSET_BASIC_DESC="Configure backup status collection for heartbeat reporting."
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_HEARTBEAT_LABEL="Include in Heartbeat"
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_HEARTBEAT_DESC="Include MokoSuiteBackup status data in heartbeat payloads sent to MokoSuiteHQ."
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_STALE_DAYS_LABEL="Stale Backup Threshold (days)"
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_STALE_DAYS_DESC="Number of days without a backup before status is marked as degraded. Default: 7."
diff --git a/source/packages/plg_system_mokosuiteclient_backup/language/en-GB/plg_system_mokosuiteclient_backup.sys.ini b/source/packages/plg_system_mokosuiteclient_backup/language/en-GB/plg_system_mokosuiteclient_backup.sys.ini
new file mode 100644
index 00000000..07da83a8
--- /dev/null
+++ b/source/packages/plg_system_mokosuiteclient_backup/language/en-GB/plg_system_mokosuiteclient_backup.sys.ini
@@ -0,0 +1,3 @@
+; MokoSuiteClient Backup Bridge Plugin - System strings
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP="System - MokoSuiteClient Backup"
+PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC="MokoSuiteBackup detection and heartbeat integration."
diff --git a/source/packages/plg_system_mokosuiteclient_backup/mokosuiteclient_backup.xml b/source/packages/plg_system_mokosuiteclient_backup/mokosuiteclient_backup.xml
new file mode 100644
index 00000000..c0f05aeb
--- /dev/null
+++ b/source/packages/plg_system_mokosuiteclient_backup/mokosuiteclient_backup.xml
@@ -0,0 +1,48 @@
+
+
+ System - MokoSuiteClient Backup
+ mokosuiteclient_backup
+ Moko Consulting
+ 2026-06-18
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GPL-3.0-or-later
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ 02.34.84-dev
+ PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC
+ Moko\Plugin\System\MokoSuiteClientBackup
+
+
+ src
+ services
+ language
+
+
+
+ en-GB/plg_system_mokosuiteclient_backup.ini
+ en-GB/plg_system_mokosuiteclient_backup.sys.ini
+
+
+
+
+
+
+
+
diff --git a/source/packages/plg_system_mokosuiteclient_backup/services/provider.php b/source/packages/plg_system_mokosuiteclient_backup/services/provider.php
new file mode 100644
index 00000000..4ea63861
--- /dev/null
+++ b/source/packages/plg_system_mokosuiteclient_backup/services/provider.php
@@ -0,0 +1,34 @@
+set(
+ PluginInterface::class,
+ function (Container $container) {
+ $dispatcher = $container->get(DispatcherInterface::class);
+ $plugin = new Backup($dispatcher, (array) PluginHelper::getPlugin('system', 'mokosuiteclient_backup'));
+ $plugin->setApplication(Factory::getApplication());
+
+ return $plugin;
+ }
+ );
+ }
+};
diff --git a/source/packages/plg_system_mokosuiteclient_backup/src/Extension/Backup.php b/source/packages/plg_system_mokosuiteclient_backup/src/Extension/Backup.php
new file mode 100644
index 00000000..446b36c8
--- /dev/null
+++ b/source/packages/plg_system_mokosuiteclient_backup/src/Extension/Backup.php
@@ -0,0 +1,263 @@
+ 'onCollectHeartbeat',
+ ];
+ }
+
+ /**
+ * Collect backup status data for the heartbeat payload.
+ *
+ * Triggered by the monitor plugin before sending a heartbeat.
+ * Appends a 'backup' key to the heartbeat data array.
+ */
+ public function onCollectHeartbeat($event): void
+ {
+ if (!$this->params->get('heartbeat_enabled', 1))
+ {
+ return;
+ }
+
+ try
+ {
+ $data = $this->getBackupStatus();
+ $event->addResult('backup', $data);
+ }
+ catch (\Throwable $e)
+ {
+ Log::add('Backup bridge: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
+
+ // Send explicit error so HQ knows collection failed,
+ // rather than interpreting absence as "not installed"
+ $event->addResult('backup', [
+ 'installed' => true,
+ 'status' => 'error',
+ 'message' => 'Failed to collect backup status',
+ ]);
+ }
+ }
+
+ /**
+ * Check if MokoSuiteBackup is installed.
+ *
+ * Queries the extensions table for the component, which is more
+ * reliable than checking for database tables alone.
+ */
+ public function isBackupInstalled(): bool
+ {
+ try
+ {
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+
+ $query = $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitebackup'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('component'));
+
+ $db->setQuery($query);
+
+ return (int) $db->loadResult() > 0;
+ }
+ catch (\Throwable $e)
+ {
+ return false;
+ }
+ }
+
+ /**
+ * Get backup status summary from MokoSuiteBackup.
+ *
+ * Prefers the BackupStatusHelper API when available. Falls back
+ * to a direct database query for compatibility with older versions.
+ *
+ * @return array Backup status data for heartbeat inclusion.
+ */
+ public function getBackupStatus(): array
+ {
+ if (!$this->isBackupInstalled())
+ {
+ return [
+ 'installed' => false,
+ 'status' => 'ok',
+ ];
+ }
+
+ // Prefer MokoSuiteBackup's own helper (clean public API)
+ $helperClass = 'Joomla\\Component\\MokoSuiteBackup\\Administrator\\Utility\\BackupStatusHelper';
+
+ if (class_exists($helperClass))
+ {
+ $staleDays = (int) $this->params->get('stale_days', 7);
+
+ return $helperClass::getStatus($staleDays);
+ }
+
+ // Fallback: direct table query for older MokoSuiteBackup versions
+ $db = Factory::getContainer()->get(DatabaseInterface::class);
+ $tables = $db->getTableList();
+ $prefix = $db->getPrefix();
+
+ if (!in_array($prefix . 'mokosuitebackup_records', $tables, true))
+ {
+ return [
+ 'installed' => true,
+ 'status' => 'degraded',
+ 'message' => 'Backup tables not found',
+ ];
+ }
+
+ return $this->queryBackupRecords($db);
+ }
+
+ /**
+ * Query MokoSuiteBackup records for the latest backup summary.
+ *
+ * Column names match the MokoSuiteBackup schema:
+ * - backupstart/backupend (not created/modified)
+ * - status: pending, running, complete, fail
+ * - total_size in bytes
+ *
+ * @param DatabaseInterface $db Database driver.
+ *
+ * @return array Backup status array.
+ */
+ private function queryBackupRecords(DatabaseInterface $db): array
+ {
+ $staleDays = (int) $this->params->get('stale_days', 7);
+
+ // Most recent backup record
+ $query = $db->getQuery(true)
+ ->select([
+ $db->quoteName('id'),
+ $db->quoteName('description'),
+ $db->quoteName('status'),
+ $db->quoteName('backup_type'),
+ $db->quoteName('total_size'),
+ $db->quoteName('backupstart'),
+ $db->quoteName('backupend'),
+ $db->quoteName('origin'),
+ $db->quoteName('filesexist'),
+ ])
+ ->from($db->quoteName('#__mokosuitebackup_records'))
+ ->order($db->quoteName('id') . ' DESC');
+
+ $db->setQuery($query, 0, 1);
+ $latest = $db->loadObject();
+
+ if (!$latest)
+ {
+ return [
+ 'installed' => true,
+ 'status' => 'degraded',
+ 'message' => 'No backups found',
+ ];
+ }
+
+ // Count completed backups
+ $db->setQuery(
+ $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__mokosuitebackup_records'))
+ ->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
+ );
+ $totalBackups = (int) $db->loadResult();
+
+ $cutoff = date('Y-m-d H:i:s', strtotime("-{$staleDays} days"));
+ $db->setQuery(
+ $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__mokosuitebackup_records'))
+ ->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
+ ->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff))
+ );
+ $recentBackups = (int) $db->loadResult();
+
+ // Failures in last 7 days
+ $db->setQuery(
+ $db->getQuery(true)
+ ->select('COUNT(*)')
+ ->from($db->quoteName('#__mokosuitebackup_records'))
+ ->where($db->quoteName('status') . ' = ' . $db->quote('fail'))
+ ->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff))
+ );
+ $failCount7d = (int) $db->loadResult();
+
+ // Determine status
+ $daysSince = 999;
+
+ if (!empty($latest->backupstart) && $latest->backupstart !== '0000-00-00 00:00:00')
+ {
+ $daysSince = (int) ((time() - strtotime($latest->backupstart)) / 86400);
+ }
+
+ $status = 'ok';
+
+ if ($latest->status === 'fail')
+ {
+ $status = 'degraded';
+ }
+ elseif ($latest->status !== 'complete')
+ {
+ $status = ($latest->status === 'running') ? 'ok' : 'degraded';
+ }
+ elseif ($daysSince > $staleDays)
+ {
+ $status = 'degraded';
+ }
+
+ $sizeMb = $latest->total_size
+ ? round($latest->total_size / 1048576)
+ : null;
+
+ return [
+ 'installed' => true,
+ 'status' => $status,
+ 'last_backup' => $latest->backupstart,
+ 'last_status' => $latest->status,
+ 'last_size_mb' => $sizeMb,
+ 'days_since' => $daysSince,
+ 'backup_type' => $latest->backup_type,
+ 'origin' => $latest->origin,
+ 'total_backups' => $totalBackups,
+ 'recent_7d' => $recentBackups,
+ 'fail_count_7d' => $failCount7d,
+ 'files_exist' => (bool) $latest->filesexist,
+ 'description' => $latest->description,
+ ];
+ }
+}
diff --git a/source/packages/plg_task_mokosuiteclientdemo/mokosuiteclientdemo.xml b/source/packages/plg_task_mokosuiteclientdemo/mokosuiteclientdemo.xml
index 153b22ca..fb33141a 100644
--- a/source/packages/plg_task_mokosuiteclientdemo/mokosuiteclientdemo.xml
+++ b/source/packages/plg_task_mokosuiteclientdemo/mokosuiteclientdemo.xml
@@ -4,8 +4,8 @@
SPDX-License-Identifier: GPL-3.0-or-later
-->
- Task - MokoSuiteClient Content Sync
- mokosuiteclientsync
+ Task - MokoSuiteClient Demo Reset
+ mokosuiteclientdemo
Moko Consulting
2026-05-30
Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -13,11 +13,11 @@
hello@mokoconsulting.tech
https://mokoconsulting.tech
02.34.84-dev
- PLG_TASK_MOKOSUITESYNC_DESC
- Moko\Plugin\Task\MokoSuiteClientSync
+ PLG_TASK_MOKOSUITEDEMO_DESC
+ Moko\Plugin\Task\MokoSuiteClientDemo
- mokosuiteclientsync.xml
+ mokosuiteclientdemo.xml
src
services
forms
@@ -25,7 +25,7 @@
- en-GB/plg_task_mokosuiteclientsync.ini
- en-GB/plg_task_mokosuiteclientsync.sys.ini
+ en-GB/plg_task_mokosuiteclientdemo.ini
+ en-GB/plg_task_mokosuiteclientdemo.sys.ini
diff --git a/source/packages/plg_task_mokosuiteclientsync/mokosuiteclientsync.xml b/source/packages/plg_task_mokosuiteclientsync/mokosuiteclientsync.xml
index fb33141a..153b22ca 100644
--- a/source/packages/plg_task_mokosuiteclientsync/mokosuiteclientsync.xml
+++ b/source/packages/plg_task_mokosuiteclientsync/mokosuiteclientsync.xml
@@ -4,8 +4,8 @@
SPDX-License-Identifier: GPL-3.0-or-later
-->
- Task - MokoSuiteClient Demo Reset
- mokosuiteclientdemo
+ Task - MokoSuiteClient Content Sync
+ mokosuiteclientsync
Moko Consulting
2026-05-30
Copyright (C) 2026 Moko Consulting. All rights reserved.
@@ -13,11 +13,11 @@
hello@mokoconsulting.tech
https://mokoconsulting.tech
02.34.84-dev
- PLG_TASK_MOKOSUITEDEMO_DESC
- Moko\Plugin\Task\MokoSuiteClientDemo
+ PLG_TASK_MOKOSUITESYNC_DESC
+ Moko\Plugin\Task\MokoSuiteClientSync
- mokosuiteclientdemo.xml
+ mokosuiteclientsync.xml
src
services
forms
@@ -25,7 +25,7 @@
- en-GB/plg_task_mokosuiteclientdemo.ini
- en-GB/plg_task_mokosuiteclientdemo.sys.ini
+ en-GB/plg_task_mokosuiteclientsync.ini
+ en-GB/plg_task_mokosuiteclientsync.sys.ini
diff --git a/source/pkg_mokosuiteclient.xml b/source/pkg_mokosuiteclient.xml
index f79089f3..613d4d30 100644
--- a/source/pkg_mokosuiteclient.xml
+++ b/source/pkg_mokosuiteclient.xml
@@ -25,6 +25,7 @@
mod_mokosuiteclient_cache.zip
mod_mokosuiteclient_categories.zip
+ plg_system_mokosuiteclient_backup.zip
plg_webservices_mokosuiteclient.zip
plg_task_mokosuiteclientdemo.zip
plg_task_mokosuiteclientsync.zip