Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 446539844d | |||
| 9b347dd136 | |||
| bca879f0d3 | |||
| 0d731eafd0 | |||
| 559db324cb | |||
| bd37187f0e | |||
| 8fd50ea580 |
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# INGROUP: mokoplatform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
@@ -43,19 +43,19 @@ jobs:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
- name: Setup mokoplatform 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/moko-platform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
if [ -d "/opt/mokoplatform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/mokoplatform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokoplatform.git" \
|
||||
/tmp/mokoplatform-api
|
||||
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokoplatform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
|
||||
@@ -109,6 +109,37 @@ jobs:
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: Update RC release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Release candidate"
|
||||
else
|
||||
NOTES="Release candidate"
|
||||
fi
|
||||
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/release-candidate" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "RC release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
@@ -685,7 +685,8 @@ class DisplayController extends BaseController
|
||||
if (!file_exists($path)) { throw new \RuntimeException('File not found', 404); }
|
||||
$app = Factory::getApplication();
|
||||
$app->setHeader('Content-Type', $att->mimetype ?: 'application/octet-stream');
|
||||
$app->setHeader('Content-Disposition', 'attachment; filename="' . $att->filename . '"');
|
||||
$safeName = str_replace(['"', "\r", "\n"], '', $att->filename);
|
||||
$app->setHeader('Content-Disposition', 'attachment; filename="' . $safeName . '"');
|
||||
$app->setHeader('Content-Length', (string) filesize($path));
|
||||
$app->sendHeaders();
|
||||
readfile($path);
|
||||
@@ -704,6 +705,7 @@ class DisplayController extends BaseController
|
||||
public function rateTicket()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$ticketId = $input->getInt('ticket_id', 0);
|
||||
$rating = $input->getInt('rating', 0);
|
||||
|
||||
@@ -20,7 +20,7 @@ class AttachmentService
|
||||
private const STORAGE_DIR = JPATH_ROOT . '/media/com_mokosuiteclient/attachments';
|
||||
|
||||
private const ALLOWED_EXTENSIONS = [
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg',
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp',
|
||||
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'txt', 'rtf',
|
||||
'zip', 'gz', 'tar',
|
||||
];
|
||||
@@ -95,7 +95,7 @@ class AttachmentService
|
||||
'filename' => $originalName,
|
||||
'filepath' => $ticketId . '/' . $storedName,
|
||||
'filesize' => $files['size'][$i],
|
||||
'mimetype' => $files['type'][$i],
|
||||
'mimetype' => mime_content_type($destPath) ?: 'application/octet-stream',
|
||||
'uploaded_by' => $userId,
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
];
|
||||
|
||||
@@ -120,13 +120,19 @@ class AutomationEngine
|
||||
{
|
||||
case 'set_status':
|
||||
if ($ticketId) {
|
||||
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET status = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
|
||||
$statusId = self::resolveStatusId($db, $value);
|
||||
$sets = "status = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())}";
|
||||
if ($statusId) { $sets .= ", status_id = {$statusId}"; }
|
||||
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'set_priority':
|
||||
if ($ticketId) {
|
||||
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET priority = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
|
||||
$priorityId = self::resolvePriorityId($db, $value);
|
||||
$sets = "priority = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())}";
|
||||
if ($priorityId) { $sets .= ", priority_id = {$priorityId}"; }
|
||||
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -167,7 +173,10 @@ class AutomationEngine
|
||||
|
||||
case 'close':
|
||||
if ($ticketId) {
|
||||
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET status = 'closed', closed = {$db->quote(Factory::getDate()->toSql())}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
|
||||
$closedId = self::resolveClosedStatusId($db);
|
||||
$sets = "status = 'closed', closed = {$db->quote(Factory::getDate()->toSql())}, modified = {$db->quote(Factory::getDate()->toSql())}";
|
||||
if ($closedId) { $sets .= ", status_id = {$closedId}"; }
|
||||
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -186,6 +195,30 @@ class AutomationEngine
|
||||
/**
|
||||
* Create a ticket from automation (with behavior: append/always_new/skip_if_open).
|
||||
*/
|
||||
private static function resolveStatusId($db, string $alias): int
|
||||
{
|
||||
return (int) $db->setQuery(
|
||||
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($alias)), 0, 1
|
||||
)->loadResult();
|
||||
}
|
||||
|
||||
private static function resolvePriorityId($db, string $alias): int
|
||||
{
|
||||
return (int) $db->setQuery(
|
||||
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_priorities')
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($alias)), 0, 1
|
||||
)->loadResult();
|
||||
}
|
||||
|
||||
private static function resolveClosedStatusId($db): int
|
||||
{
|
||||
return (int) $db->setQuery(
|
||||
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
|
||||
->where($db->quoteName('is_closed') . ' = 1'), 0, 1
|
||||
)->loadResult();
|
||||
}
|
||||
|
||||
private static function createTicketFromAutomation(object $rule, array $context, string $subject): void
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
@@ -195,12 +228,13 @@ class AutomationEngine
|
||||
|
||||
if ($behavior !== 'always_new' && $userId > 0)
|
||||
{
|
||||
// Check for existing open ticket
|
||||
// Check for existing open ticket (check both status ENUM and status_id)
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from('#__mokosuiteclient_tickets')
|
||||
->where('created_by = ' . $userId)
|
||||
->where("status NOT IN ('closed', 'resolved')");
|
||||
->select('t.id')
|
||||
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuiteclient_ticket_statuses', 's') . ' ON t.status_id = s.id')
|
||||
->where('t.created_by = ' . $userId)
|
||||
->where("(s.id IS NULL AND t.status NOT IN ('closed', 'resolved')) OR (s.id IS NOT NULL AND s.is_closed = 0)");
|
||||
|
||||
if ($catId > 0) {
|
||||
$query->where('category_id = ' . $catId);
|
||||
@@ -227,14 +261,18 @@ class AutomationEngine
|
||||
}
|
||||
|
||||
// Create new ticket
|
||||
$openStatusId = self::resolveStatusId($db, 'open') ?: null;
|
||||
$normalPriorityId = self::resolvePriorityId($db, $context['priority'] ?? 'normal') ?: null;
|
||||
$ticket = (object) [
|
||||
'subject' => $subject ?: 'Automation: ' . ($rule->title ?? ''),
|
||||
'body' => $context['body'] ?? '',
|
||||
'status' => 'open',
|
||||
'priority' => $context['priority'] ?? 'normal',
|
||||
'category_id' => $catId ?: null,
|
||||
'created_by' => $userId,
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
'subject' => $subject ?: 'Automation: ' . ($rule->title ?? ''),
|
||||
'body' => $context['body'] ?? '',
|
||||
'status' => 'open',
|
||||
'status_id' => $openStatusId,
|
||||
'priority' => $context['priority'] ?? 'normal',
|
||||
'priority_id' => $normalPriorityId,
|
||||
'category_id' => $catId ?: null,
|
||||
'created_by' => $userId,
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
];
|
||||
$db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id');
|
||||
}
|
||||
|
||||
@@ -144,13 +144,30 @@ class TicketsController extends BaseController
|
||||
return;
|
||||
}
|
||||
|
||||
$statusId = $input->getInt('status_id', 0) ?: null;
|
||||
$priorityId = $input->getInt('priority_id', 0) ?: null;
|
||||
$status = $input->getString('status', 'open');
|
||||
$priority = $input->getString('priority', 'normal');
|
||||
|
||||
// Resolve status_id from alias if not provided
|
||||
if (!$statusId && $status) {
|
||||
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($status));
|
||||
$statusId = (int) $db->setQuery($q, 0, 1)->loadResult() ?: null;
|
||||
}
|
||||
if (!$priorityId && $priority) {
|
||||
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_priorities')
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($priority));
|
||||
$priorityId = (int) $db->setQuery($q, 0, 1)->loadResult() ?: null;
|
||||
}
|
||||
|
||||
$ticket = (object) [
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
'status' => 'open',
|
||||
'status_id' => $input->getInt('status_id', 0) ?: null,
|
||||
'priority' => $input->getString('priority', 'normal'),
|
||||
'priority_id' => $input->getInt('priority_id', 0) ?: null,
|
||||
'status' => $status,
|
||||
'status_id' => $statusId,
|
||||
'priority' => $priority,
|
||||
'priority_id' => $priorityId,
|
||||
'category_id' => $input->getInt('category_id', 0) ?: null,
|
||||
'created_by' => (int) Factory::getUser()->id,
|
||||
'assigned_to' => $input->getInt('assigned_to', 0) ?: null,
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
; MokoSuiteClient Backup Bridge Plugin
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP="System - MokoSuiteClient Backup"
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC="Detects MokoSuiteBackup and includes backup status in heartbeat payloads sent to MokoSuiteHQ."
|
||||
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_FIELDSET_BASIC="Backup Monitoring"
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_FIELDSET_BASIC_DESC="Configure backup status collection for heartbeat reporting."
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_HEARTBEAT_LABEL="Include in Heartbeat"
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_HEARTBEAT_DESC="Include MokoSuiteBackup status data in heartbeat payloads sent to MokoSuiteHQ."
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_STALE_DAYS_LABEL="Stale Backup Threshold (days)"
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_STALE_DAYS_DESC="Number of days without a backup before status is marked as degraded. Default: 7."
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
; MokoSuiteClient Backup Bridge Plugin - System strings
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP="System - MokoSuiteClient Backup"
|
||||
PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC="MokoSuiteBackup detection and heartbeat integration."
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteClient Backup</name>
|
||||
<element>mokosuiteclient_backup</element>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-06-18</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.84-dev</version>
|
||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientBackup</namespace>
|
||||
|
||||
<files>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/plg_system_mokosuiteclient_backup.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_system_mokosuiteclient_backup.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic"
|
||||
label="PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_FIELDSET_BASIC"
|
||||
description="PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_FIELDSET_BASIC_DESC">
|
||||
|
||||
<field name="heartbeat_enabled" type="radio" default="1"
|
||||
label="PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_HEARTBEAT_LABEL"
|
||||
description="PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_HEARTBEAT_DESC"
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field name="stale_days" type="number" default="7"
|
||||
label="PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_STALE_DAYS_LABEL"
|
||||
description="PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_STALE_DAYS_DESC"
|
||||
min="1" max="90" step="1" />
|
||||
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuiteClient
|
||||
* @subpackage plg_system_mokosuiteclient_backup
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Moko\Plugin\System\MokoSuiteClientBackup\Extension\Backup;
|
||||
|
||||
return new class implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$dispatcher = $container->get(DispatcherInterface::class);
|
||||
$plugin = new Backup($dispatcher, (array) PluginHelper::getPlugin('system', 'mokosuiteclient_backup'));
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuiteClient
|
||||
* @subpackage plg_system_mokosuiteclient_backup
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteClientBackup\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
/**
|
||||
* MokoSuiteClient Backup Bridge Plugin
|
||||
*
|
||||
* Detects whether MokoSuiteBackup is installed and collects backup
|
||||
* status data for inclusion in heartbeat payloads to MokoSuiteHQ.
|
||||
*
|
||||
* Prefers MokoSuiteBackup's own BackupStatusHelper when available,
|
||||
* falling back to a direct table query if the helper class is missing
|
||||
* (e.g. older versions of MokoSuiteBackup).
|
||||
*
|
||||
* @since 02.34.84
|
||||
*/
|
||||
class Backup extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
protected $autoloadLanguage = true;
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'onMokoSuiteClientCollectHeartbeat' => 'onCollectHeartbeat',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect backup status data for the heartbeat payload.
|
||||
*
|
||||
* Triggered by the monitor plugin before sending a heartbeat.
|
||||
* Appends a 'backup' key to the heartbeat data array.
|
||||
*/
|
||||
public function onCollectHeartbeat($event): void
|
||||
{
|
||||
if (!$this->params->get('heartbeat_enabled', 1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$data = $this->getBackupStatus();
|
||||
$event->addResult('backup', $data);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Backup bridge: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
||||
|
||||
// Send explicit error so HQ knows collection failed,
|
||||
// rather than interpreting absence as "not installed"
|
||||
$event->addResult('backup', [
|
||||
'installed' => true,
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to collect backup status',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if MokoSuiteBackup is installed.
|
||||
*
|
||||
* Queries the extensions table for the component, which is more
|
||||
* reliable than checking for database tables alone.
|
||||
*/
|
||||
public function isBackupInstalled(): 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup status summary from MokoSuiteBackup.
|
||||
*
|
||||
* Prefers the BackupStatusHelper API when available. Falls back
|
||||
* to a direct database query for compatibility with older versions.
|
||||
*
|
||||
* @return array Backup status data for heartbeat inclusion.
|
||||
*/
|
||||
public function getBackupStatus(): array
|
||||
{
|
||||
if (!$this->isBackupInstalled())
|
||||
{
|
||||
return [
|
||||
'installed' => false,
|
||||
'status' => 'ok',
|
||||
];
|
||||
}
|
||||
|
||||
// Prefer MokoSuiteBackup's own helper (clean public API)
|
||||
$helperClass = 'Joomla\\Component\\MokoSuiteBackup\\Administrator\\Utility\\BackupStatusHelper';
|
||||
|
||||
if (class_exists($helperClass))
|
||||
{
|
||||
$staleDays = (int) $this->params->get('stale_days', 7);
|
||||
|
||||
return $helperClass::getStatus($staleDays);
|
||||
}
|
||||
|
||||
// Fallback: direct table query for older MokoSuiteBackup versions
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$tables = $db->getTableList();
|
||||
$prefix = $db->getPrefix();
|
||||
|
||||
if (!in_array($prefix . 'mokosuitebackup_records', $tables, true))
|
||||
{
|
||||
return [
|
||||
'installed' => true,
|
||||
'status' => 'degraded',
|
||||
'message' => 'Backup tables not found',
|
||||
];
|
||||
}
|
||||
|
||||
return $this->queryBackupRecords($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query MokoSuiteBackup records for the latest backup summary.
|
||||
*
|
||||
* Column names match the MokoSuiteBackup schema:
|
||||
* - backupstart/backupend (not created/modified)
|
||||
* - status: pending, running, complete, fail
|
||||
* - total_size in bytes
|
||||
*
|
||||
* @param DatabaseInterface $db Database driver.
|
||||
*
|
||||
* @return array Backup status array.
|
||||
*/
|
||||
private function queryBackupRecords(DatabaseInterface $db): array
|
||||
{
|
||||
$staleDays = (int) $this->params->get('stale_days', 7);
|
||||
|
||||
// Most recent backup record
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('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',
|
||||
];
|
||||
}
|
||||
|
||||
// Count completed backups
|
||||
$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("-{$staleDays} 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 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteClient Content Sync</name>
|
||||
<element>mokosuiteclientsync</element>
|
||||
<name>Task - MokoSuiteClient Demo Reset</name>
|
||||
<element>mokosuiteclientdemo</element>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
@@ -13,11 +13,11 @@
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.84-dev</version>
|
||||
<description>PLG_TASK_MOKOSUITESYNC_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientSync</namespace>
|
||||
<description>PLG_TASK_MOKOSUITEDEMO_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientDemo</namespace>
|
||||
|
||||
<files>
|
||||
<filename plugin="mokosuiteclientsync">mokosuiteclientsync.xml</filename>
|
||||
<filename plugin="mokosuiteclientdemo">mokosuiteclientdemo.xml</filename>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>forms</folder>
|
||||
@@ -25,7 +25,7 @@
|
||||
</files>
|
||||
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/plg_task_mokosuiteclientsync.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_task_mokosuiteclientsync.sys.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_task_mokosuiteclientdemo.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_task_mokosuiteclientdemo.sys.ini</language>
|
||||
</languages>
|
||||
</extension>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteClient Demo Reset</name>
|
||||
<element>mokosuiteclientdemo</element>
|
||||
<name>Task - MokoSuiteClient Content Sync</name>
|
||||
<element>mokosuiteclientsync</element>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
@@ -13,11 +13,11 @@
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.84-dev</version>
|
||||
<description>PLG_TASK_MOKOSUITEDEMO_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientDemo</namespace>
|
||||
<description>PLG_TASK_MOKOSUITESYNC_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientSync</namespace>
|
||||
|
||||
<files>
|
||||
<filename plugin="mokosuiteclientdemo">mokosuiteclientdemo.xml</filename>
|
||||
<filename plugin="mokosuiteclientsync">mokosuiteclientsync.xml</filename>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>forms</folder>
|
||||
@@ -25,7 +25,7 @@
|
||||
</files>
|
||||
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/plg_task_mokosuiteclientdemo.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_task_mokosuiteclientdemo.sys.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_task_mokosuiteclientsync.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_task_mokosuiteclientsync.sys.ini</language>
|
||||
</languages>
|
||||
</extension>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<file type="module" id="mod_mokosuiteclient_menu" client="administrator">mod_mokosuiteclient_menu.zip</file>
|
||||
<file type="module" id="mod_mokosuiteclient_cache" client="administrator">mod_mokosuiteclient_cache.zip</file>
|
||||
<file type="module" id="mod_mokosuiteclient_categories" client="administrator">mod_mokosuiteclient_categories.zip</file>
|
||||
<file type="plugin" id="plg_system_mokosuiteclient_backup" group="system">plg_system_mokosuiteclient_backup.zip</file>
|
||||
<file type="plugin" id="plg_webservices_mokosuiteclient" group="webservices">plg_webservices_mokosuiteclient.zip</file>
|
||||
<file type="plugin" id="plg_task_mokosuiteclientdemo" group="task">plg_task_mokosuiteclientdemo.zip</file>
|
||||
<file type="plugin" id="plg_task_mokosuiteclientsync" group="task">plg_task_mokosuiteclientsync.zip</file>
|
||||
|
||||
Reference in New Issue
Block a user