feat: add BackupStatusHelper for bridge integration (#47)
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 32s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 35s

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
This commit is contained in:
Jonathan Miller
2026-06-20 18:23:07 -05:00
parent 70f25f6e79
commit a5a2f48e7c
@@ -0,0 +1,180 @@
<?php
/**
* @package MokoSuiteBackup
* @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @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,
],
];
}
}