* @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, ], ]; } }