feat: add BackupStatusHelper for bridge integration #53

Merged
jmiller merged 2 commits from feature/47-backup-status-helper into main 2026-06-20 23:31:12 +00:00
16 changed files with 1478 additions and 1298 deletions
+66 -66
View File
@@ -1,66 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
- rc
- 'feature/**'
- 'patch/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup mokocli tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/mokocli/cli" ]; then
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
/tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
- rc
- 'feature/**'
- 'patch/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup mokocli tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/mokocli/cli" ]; then
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
/tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 01.00.00
# VERSION: 01.24.01
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
# MokoSuiteBackup
<!-- VERSION: 01.24.00 -->
<!-- VERSION: 01.24.01 -->
Full-site backup and restore for Joomla — database, files, and configuration.
+1 -1
View File
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="component" method="upgrade">
<name>MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -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,
],
];
}
}
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="console" method="upgrade">
<name>Console - MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename>
<version>01.24.00</version>
<version>01.24.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>