From 937e38bcc65368aea9df8b802f115db36107955d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 18 Jun 2026 10:40:56 -0500 Subject: [PATCH 1/3] feat(utility): add BackupStatusHelper for external plugin integration (#47) Public API for MokoSuiteClient bridge plugin to query backup status without depending on internal model structure. Provides getStatus() and isInstalled() static methods. --- .../src/Utility/BackupStatusHelper.php | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 source/packages/com_mokosuitebackup/src/Utility/BackupStatusHelper.php diff --git a/source/packages/com_mokosuitebackup/src/Utility/BackupStatusHelper.php b/source/packages/com_mokosuitebackup/src/Utility/BackupStatusHelper.php new file mode 100644 index 0000000..2e356b2 --- /dev/null +++ b/source/packages/com_mokosuitebackup/src/Utility/BackupStatusHelper.php @@ -0,0 +1,175 @@ + + * @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\Utility; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Database\DatabaseInterface; + +/** + * Provides a public API for external plugins (e.g. MokoSuiteClient bridge) + * to query backup status without depending on internal model internals. + * + * @since 01.22.07 + */ +class BackupStatusHelper +{ + /** + * Get a summary of the latest backup status. + * + * Returns an array suitable for inclusion in heartbeat payloads. + * + * @param int $staleDays Days without backup before status is degraded. + * + * @return array + */ + public static function getStatus(int $staleDays = 7): array + { + try + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + catch (\Throwable $e) + { + return ['installed' => true, 'status' => 'error', 'message' => 'Database unavailable']; + } + + // Latest backup record (any status) + $query = $db->getQuery(true) + ->select([ + $db->quoteName('id'), + $db->quoteName('profile_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', + ]; + } + + // Counts + $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('-7 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 overall 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, + ]; + } + + /** + * Check if MokoSuiteBackup component is installed. + * + * Useful for external plugins that want to check before calling getStatus(). + * + * @return bool + */ + public static function isInstalled(): 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; + } + } +} -- 2.52.0 From a77458a24a40af4b2e5c804f98ab0fa1a4e1f5e0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 18 Jun 2026 11:13:12 -0500 Subject: [PATCH 2/3] fix(status): use staleDays param for cutoff, remove unused profile_id (#47) - Use $staleDays instead of hardcoded 7-day cutoff for recent/failure counts so the parameter is consistently applied - Remove unused profile_id from SELECT (never included in return array) --- .../com_mokosuitebackup/src/Utility/BackupStatusHelper.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/source/packages/com_mokosuitebackup/src/Utility/BackupStatusHelper.php b/source/packages/com_mokosuitebackup/src/Utility/BackupStatusHelper.php index 2e356b2..ba7b4aa 100644 --- a/source/packages/com_mokosuitebackup/src/Utility/BackupStatusHelper.php +++ b/source/packages/com_mokosuitebackup/src/Utility/BackupStatusHelper.php @@ -42,11 +42,10 @@ class BackupStatusHelper return ['installed' => true, 'status' => 'error', 'message' => 'Database unavailable']; } - // Latest backup record (any status) + // Most recently inserted backup record (by ID, any status) $query = $db->getQuery(true) ->select([ $db->quoteName('id'), - $db->quoteName('profile_id'), $db->quoteName('description'), $db->quoteName('status'), $db->quoteName('backup_type'), @@ -71,7 +70,6 @@ class BackupStatusHelper ]; } - // Counts $db->setQuery( $db->getQuery(true) ->select('COUNT(*)') @@ -80,7 +78,7 @@ class BackupStatusHelper ); $totalBackups = (int) $db->loadResult(); - $cutoff = date('Y-m-d H:i:s', strtotime('-7 days')); + $cutoff = date('Y-m-d H:i:s', strtotime("-{$staleDays} days")); $db->setQuery( $db->getQuery(true) ->select('COUNT(*)') -- 2.52.0 From 896c4c597ec32c8aa452f7bb972e103fc5a0f24d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 18 Jun 2026 12:46:53 -0500 Subject: [PATCH 3/3] ci: retrigger after runner restart -- 2.52.0