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_menu.zip 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