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..6eee0741 --- /dev/null +++ b/source/packages/plg_system_mokosuiteclient_backup/src/Extension/Backup.php @@ -0,0 +1,219 @@ + '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'); + } + } + + /** + * 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. + * + * @return array Backup status data for heartbeat inclusion. + */ + public function getBackupStatus(): array + { + if (!$this->isBackupInstalled()) + { + return [ + 'installed' => false, + ]; + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + $tables = $db->getTableList(); + $prefix = $db->getPrefix(); + $statsTable = $prefix . 'mokosuitebackup_records'; + + if (!in_array($statsTable, $tables, true)) + { + return [ + 'installed' => true, + 'status' => 'degraded', + 'message' => 'Backup tables not found', + ]; + } + + // TODO: Query MokoSuiteBackup records table for latest backup status. + // + // This is a placeholder — the actual column names and table structure + // depend on MokoSuiteBackup's schema. Once that component is available + // locally, update this query to match its database layout. + // + // Expected return shape: + // [ + // 'installed' => true, + // 'status' => 'ok' | 'degraded', + // 'last_backup' => '2026-06-18 10:30:45', + // 'last_status' => 'complete' | 'failed' | 'partial', + // 'last_size_mb' => 512, + // 'days_since' => 2, + // 'total_backups'=> 42, + // 'recent_7d' => 5, + // 'destination' => 'local' | 's3' | 'remote', + // 'description' => 'Full site backup', + // ] + + return $this->queryBackupRecords($db); + } + + /** + * Query MokoSuiteBackup records for the latest backup summary. + * + * @param DatabaseInterface $db Database driver. + * + * @return array Backup status array. + */ + private function queryBackupRecords(DatabaseInterface $db): array + { + $staleDays = (int) $this->params->get('stale_days', 7); + + // Get the most recent backup record + $query = $db->getQuery(true) + ->select('*') + ->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 total and recent backups + $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitebackup_records')) + ); + $totalBackups = (int) $db->loadResult(); + + $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitebackup_records')) + ->where($db->quoteName('created') . ' >= DATE_SUB(NOW(), INTERVAL 7 DAY)') + ); + $recentBackups = (int) $db->loadResult(); + + // Determine status + $lastDate = $latest->created ?? ''; + $daysSince = $lastDate ? (int) ((time() - strtotime($lastDate)) / 86400) : 999; + $lastStatus = $latest->status ?? 'unknown'; + + $status = 'ok'; + + if ($lastStatus !== 'complete') + { + $status = 'degraded'; + } + elseif ($daysSince > $staleDays) + { + $status = 'degraded'; + } + + $sizeMb = !empty($latest->total_size) + ? round($latest->total_size / 1048576) + : null; + + return [ + 'installed' => true, + 'status' => $status, + 'last_backup' => $lastDate, + 'last_status' => $lastStatus, + 'last_size_mb' => $sizeMb, + 'days_since' => $daysSince, + 'total_backups' => $totalBackups, + 'recent_7d' => $recentBackups, + 'destination' => $latest->destination ?? null, + 'description' => $latest->description ?? null, + ]; + } +} 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