From a5a2f48e7c4f778ca675fc9d180eda819e8be08b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 20 Jun 2026 18:23:07 -0500 Subject: [PATCH] feat: add BackupStatusHelper for bridge integration (#47) Static helper class for external consumers (bridge plugins, MCP servers) to query backup status without bootstrapping the full component. Methods: - isInstalled(): check if MokoSuiteBackup is installed and enabled - getLatestRecord(): get the most recent completed/failed backup - getStatusSummary(): full heartbeat payload with latest status, all-time/7-day totals, and consecutive success streak --- .../src/Helper/BackupStatusHelper.php | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php diff --git a/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php b/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php new file mode 100644 index 0000000..7de26bc --- /dev/null +++ b/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php @@ -0,0 +1,180 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoSuiteBackup\Administrator\Helper; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; + +/** + * Lightweight helper for external consumers (bridge plugins, MCP servers, CLI tools) + * to query MokoSuiteBackup status without bootstrapping the full component. + * + * Usage from any Joomla plugin: + * \Joomla\Component\MokoSuiteBackup\Administrator\Helper\BackupStatusHelper::getStatusSummary() + */ +class BackupStatusHelper +{ + /** + * Check whether MokoSuiteBackup is installed and enabled. + */ + public static function isInstalled(): bool + { + $db = Factory::getContainer()->get('DatabaseDriver'); + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokosuitebackup')) + ->where($db->quoteName('enabled') . ' = 1'); + + return (int) $db->setQuery($query)->loadResult() > 0; + } + + /** + * Get the latest backup record for a given profile (or any profile). + * + * @param int|null $profileId Limit to a specific profile, or null for any. + * @return object|null Record object or null if no backups exist. + */ + public static function getLatestRecord(?int $profileId = null): ?object + { + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select([ + $db->quoteName('r.id'), + $db->quoteName('r.profile_id'), + $db->quoteName('r.description'), + $db->quoteName('r.status'), + $db->quoteName('r.origin'), + $db->quoteName('r.backup_type'), + $db->quoteName('r.archivename'), + $db->quoteName('r.total_size'), + $db->quoteName('r.db_size'), + $db->quoteName('r.files_count'), + $db->quoteName('r.tables_count'), + $db->quoteName('r.backupstart'), + $db->quoteName('r.backupend'), + $db->quoteName('r.filesexist'), + $db->quoteName('r.remote_filename'), + $db->quoteName('r.checksum'), + $db->quoteName('p.title', 'profile_title'), + ]) + ->from($db->quoteName('#__mokosuitebackup_records', 'r')) + ->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id') + ->where($db->quoteName('r.status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'fail'])) . ')') + ->order($db->quoteName('r.backupstart') . ' DESC'); + + if ($profileId !== null) { + $query->where($db->quoteName('r.profile_id') . ' = ' . (int) $profileId); + } + + $query->setLimit(1); + $record = $db->setQuery($query)->loadObject(); + + return $record ?: null; + } + + /** + * Get a full status summary for heartbeat payloads. + * + * Returns an array suitable for JSON encoding in bridge heartbeats: + * - latest backup status, time, size, destination + * - total and recent (7-day) backup counts + * - streak of consecutive successes + * + * @return array{installed: bool, latest: ?array, totals: array} + */ + public static function getStatusSummary(): array + { + if (!self::isInstalled()) { + return ['installed' => false, 'latest' => null, 'totals' => []]; + } + + $db = Factory::getContainer()->get('DatabaseDriver'); + + // Latest completed/failed backup + $latest = self::getLatestRecord(); + $latestArray = null; + + if ($latest) { + $latestArray = [ + 'status' => $latest->status, + 'backup_type' => $latest->backup_type, + 'description' => $latest->description, + 'backup_start' => $latest->backupstart, + 'backup_end' => $latest->backupend, + 'total_size' => (int) $latest->total_size, + 'destination' => $latest->remote_filename ? 'remote' : 'local', + 'profile' => $latest->profile_title, + 'origin' => $latest->origin, + 'files_count' => (int) $latest->files_count, + 'tables_count' => (int) $latest->tables_count, + ]; + } + + // Totals + $query = $db->getQuery(true) + ->select([ + 'COUNT(*) AS total', + 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('complete') . ' THEN 1 ELSE 0 END) AS success', + 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('fail') . ' THEN 1 ELSE 0 END) AS failed', + ]) + ->from($db->quoteName('#__mokosuitebackup_records')); + + $allTime = $db->setQuery($query)->loadObject(); + + // Recent (last 7 days) + $query = $db->getQuery(true) + ->select([ + 'COUNT(*) AS total', + 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('complete') . ' THEN 1 ELSE 0 END) AS success', + 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('fail') . ' THEN 1 ELSE 0 END) AS failed', + ]) + ->from($db->quoteName('#__mokosuitebackup_records')) + ->where($db->quoteName('backupstart') . ' >= DATE_SUB(NOW(), INTERVAL 7 DAY)'); + + $recent = $db->setQuery($query)->loadObject(); + + // Success streak — count consecutive successes from latest backward + $query = $db->getQuery(true) + ->select($db->quoteName('status')) + ->from($db->quoteName('#__mokosuitebackup_records')) + ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'fail'])) . ')') + ->order($db->quoteName('backupstart') . ' DESC') + ->setLimit(50); + + $statuses = $db->setQuery($query)->loadColumn(); + $streak = 0; + + foreach ($statuses as $s) { + if ($s === 'complete') { + $streak++; + } else { + break; + } + } + + return [ + 'installed' => true, + 'latest' => $latestArray, + 'totals' => [ + 'all_time' => (int) ($allTime->total ?? 0), + 'all_success' => (int) ($allTime->success ?? 0), + 'all_failed' => (int) ($allTime->failed ?? 0), + 'recent_total' => (int) ($recent->total ?? 0), + 'recent_success' => (int) ($recent->success ?? 0), + 'recent_failed' => (int) ($recent->failed ?? 0), + 'success_streak' => $streak, + ], + ]; + } +}