From aefa46e0c40ec17e3b82ad398214ca896e284d05 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 30 Jun 2026 14:14:06 -0500 Subject: [PATCH] fix: use warning status when backup succeeds but remote upload fails Previously a successful backup with a failed remote upload was marked as "complete", hiding the upload failure. Now these records get a "warning" status with a yellow badge so operators can see at a glance which backups didn't reach their remote destination. Warning-status records are treated as usable backups throughout: - Downloadable, browsable, and restorable (the archive is intact) - Counted in dashboard stats, storage totals, and success streaks - Included in purge operations and differential base lookups - Shown with yellow "warning" badge in list, detail, and cpanel module - Filterable via the status dropdown on Backup Records Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG --- .../forms/filter_backups.xml | 1 + .../language/en-GB/com_mokosuitebackup.ini | 1 + .../language/en-US/com_mokosuitebackup.ini | 3 +++ .../com_mokosuitebackup/sql/install.mysql.sql | 2 +- .../src/Controller/AjaxController.php | 4 ++-- .../src/Controller/BackupsController.php | 2 +- .../src/Engine/BackupEngine.php | 4 ++-- .../src/Engine/PreflightCheck.php | 2 +- .../src/Engine/RestoreEngine.php | 2 +- .../src/Engine/SteppedBackupEngine.php | 2 +- .../src/Engine/SteppedRestoreEngine.php | 2 +- .../src/Helper/BackupStatusHelper.php | 6 +++--- .../src/Model/DashboardModel.php | 6 +++--- .../src/View/Backup/HtmlView.php | 2 +- .../tmpl/backup/default.php | 1 + .../tmpl/backups/default.php | 1 + .../en-GB/mod_mokosuitebackup_cpanel.ini | 1 + .../tmpl/default.php | 18 ++++++++++++++---- 18 files changed, 39 insertions(+), 21 deletions(-) diff --git a/source/packages/com_mokosuitebackup/forms/filter_backups.xml b/source/packages/com_mokosuitebackup/forms/filter_backups.xml index a44abd5..fa78e99 100644 --- a/source/packages/com_mokosuitebackup/forms/filter_backups.xml +++ b/source/packages/com_mokosuitebackup/forms/filter_backups.xml @@ -15,6 +15,7 @@ > + diff --git a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini index 738fb5a..c21b6d1 100644 --- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini @@ -207,6 +207,7 @@ COM_MOKOJOOMBACKUP_TYPE_DIFFERENTIAL="Differential (changed files + full DB)" ; Status labels COM_MOKOJOOMBACKUP_STATUS_COMPLETE="Complete" +COM_MOKOJOOMBACKUP_STATUS_WARNING="Warning" COM_MOKOJOOMBACKUP_STATUS_RUNNING="Running" COM_MOKOJOOMBACKUP_STATUS_FAIL="Failed" COM_MOKOJOOMBACKUP_STATUS_PENDING="Pending" diff --git a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini index 1d7328d..db5e85b 100644 --- a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini @@ -123,6 +123,9 @@ COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED="No backup records selected." COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING="None of the selected backups are in running status." COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled." +; Backup status +COM_MOKOJOOMBACKUP_STATUS_WARNING="Warning" + ; ACL - Cancel COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup" COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files." diff --git a/source/packages/com_mokosuitebackup/sql/install.mysql.sql b/source/packages/com_mokosuitebackup/sql/install.mysql.sql index 984ff45..705ebfd 100644 --- a/source/packages/com_mokosuitebackup/sql/install.mysql.sql +++ b/source/packages/com_mokosuitebackup/sql/install.mysql.sql @@ -65,7 +65,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_records` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `profile_id` INT(11) UNSIGNED NOT NULL DEFAULT 1, `description` VARCHAR(255) NOT NULL DEFAULT '', - `status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, running, complete, fail', + `status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, running, complete, warning, fail', `origin` VARCHAR(20) NOT NULL DEFAULT 'backend' COMMENT 'backend, cli, api, scheduled', `backup_type` VARCHAR(20) NOT NULL DEFAULT 'full' COMMENT 'full, database, files', `archivename` VARCHAR(512) NOT NULL DEFAULT '', diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php index f064e0b..264464f 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php @@ -512,7 +512,7 @@ class AjaxController extends BaseController return; } - if ($record->status !== 'complete' || !$record->filesexist) { + if (!\in_array($record->status, ['complete', 'warning'], true) || !$record->filesexist) { $this->sendJson(['error' => true, 'message' => 'Archive not available']); return; @@ -808,7 +808,7 @@ class AjaxController extends BaseController ->select('COUNT(*)') ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)) - ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')'); $db->setQuery($query); $count = (int) $db->loadResult(); } catch (\Exception $e) { diff --git a/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php index b2bad20..59a725b 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php @@ -199,7 +199,7 @@ class BackupsController extends AdminController ->select($db->quoteName('id')) ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)) - ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')'); $db->setQuery($query); $ids = $db->loadColumn(); diff --git a/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php index aaf8a56..04b8f33 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php @@ -375,7 +375,7 @@ class BackupEngine // Final record update (includes fields needed by NotificationSender) $update = (object) [ 'id' => $recordId, - 'status' => 'complete', + 'status' => $uploadFailed ? 'warning' : 'complete', 'description' => $description, 'backup_type' => $profile->backup_type, 'archivename' => $archiveName, @@ -606,7 +606,7 @@ class BackupEngine ->select($db->quoteName('manifest')) ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('profile_id') . ' = ' . $profileId) - ->where($db->quoteName('status') . ' = ' . $db->quote('complete')) + ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')') ->where($db->quoteName('manifest') . ' != ' . $db->quote('')) ->where($db->quoteName('backup_type') . ' = ' . $db->quote('full')) ->order($db->quoteName('backupstart') . ' DESC'); diff --git a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php index 64b42b5..664a3e8 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php +++ b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php @@ -165,7 +165,7 @@ class PreflightCheck ->select($db->quoteName('total_size')) ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id) - ->where($db->quoteName('status') . ' = ' . $db->quote('complete')) + ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')') ->where($db->quoteName('total_size') . ' > 0') ->order($db->quoteName('backupstart') . ' DESC'); $db->setQuery($query, 0, 1); diff --git a/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php b/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php index 55202d0..88ad222 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/RestoreEngine.php @@ -67,7 +67,7 @@ class RestoreEngine return ['success' => false, 'message' => 'Backup record not found: ' . $recordId]; } - if ($record->status !== 'complete') { + if ($record->status !== 'complete' && $record->status !== 'warning') { return ['success' => false, 'message' => 'Cannot restore from incomplete backup (status: ' . $record->status . ')']; } diff --git a/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php index a9dbb08..ee640e7 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php @@ -647,7 +647,7 @@ class SteppedBackupEngine $update = (object) [ 'id' => $session->recordId, - 'status' => 'complete', + 'status' => $uploadFailed ? 'warning' : 'complete', 'backupend' => date('Y-m-d H:i:s'), 'total_size' => $totalSize, 'checksum' => $checksum, diff --git a/source/packages/com_mokosuitebackup/src/Engine/SteppedRestoreEngine.php b/source/packages/com_mokosuitebackup/src/Engine/SteppedRestoreEngine.php index cf1b9ef..25668b1 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/SteppedRestoreEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/SteppedRestoreEngine.php @@ -64,7 +64,7 @@ class SteppedRestoreEngine return ['error' => true, 'message' => 'Backup record not found: ' . $recordId]; } - if ($record->status !== 'complete') { + if ($record->status !== 'complete' && $record->status !== 'warning') { return ['error' => true, 'message' => 'Cannot restore from incomplete backup (status: ' . $record->status . ')']; } diff --git a/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php b/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php index 7de26bc..a711f97 100644 --- a/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php +++ b/source/packages/com_mokosuitebackup/src/Helper/BackupStatusHelper.php @@ -70,7 +70,7 @@ class BackupStatusHelper ]) ->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'])) . ')') + ->where($db->quoteName('r.status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning', 'fail'])) . ')') ->order($db->quoteName('r.backupstart') . ' DESC'); if ($profileId !== null) { @@ -148,7 +148,7 @@ class BackupStatusHelper $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'])) . ')') + ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning', 'fail'])) . ')') ->order($db->quoteName('backupstart') . ' DESC') ->setLimit(50); @@ -156,7 +156,7 @@ class BackupStatusHelper $streak = 0; foreach ($statuses as $s) { - if ($s === 'complete') { + if ($s === 'complete' || $s === 'warning') { $streak++; } else { break; diff --git a/source/packages/com_mokosuitebackup/src/Model/DashboardModel.php b/source/packages/com_mokosuitebackup/src/Model/DashboardModel.php index ae85d6e..8ad63df 100644 --- a/source/packages/com_mokosuitebackup/src/Model/DashboardModel.php +++ b/source/packages/com_mokosuitebackup/src/Model/DashboardModel.php @@ -30,7 +30,7 @@ class DashboardModel extends BaseDatabaseModel ->select('r.*, p.title AS 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') . ' = ' . $db->quote('complete')) + ->where($db->quoteName('r.status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')') ->order($db->quoteName('r.backupend') . ' DESC'); $db->setQuery($query, 0, 1); @@ -75,7 +75,7 @@ class DashboardModel extends BaseDatabaseModel ->select('COUNT(*) AS total_count') ->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size') ->from($db->quoteName('#__mokosuitebackup_records')) - ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + ->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')'); $db->setQuery($query); $stats = $db->loadObject(); @@ -274,7 +274,7 @@ class DashboardModel extends BaseDatabaseModel ->select('COALESCE(SUM(r.total_size), 0) AS total_size') ->from($db->quoteName('#__mokosuitebackup_records', 'r')) ->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id') - ->where($db->quoteName('r.status') . ' = ' . $db->quote('complete')) + ->where($db->quoteName('r.status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')') ->group($db->quoteName('r.profile_id')) ->order('total_size DESC'); $db->setQuery($query); diff --git a/source/packages/com_mokosuitebackup/src/View/Backup/HtmlView.php b/source/packages/com_mokosuitebackup/src/View/Backup/HtmlView.php index cdc2c7f..d1fe0c9 100644 --- a/source/packages/com_mokosuitebackup/src/View/Backup/HtmlView.php +++ b/source/packages/com_mokosuitebackup/src/View/Backup/HtmlView.php @@ -41,7 +41,7 @@ class HtmlView extends BaseHtmlView $user = Factory::getApplication()->getIdentity(); - if ($this->item->status === 'complete' + if (\in_array($this->item->status, ['complete', 'warning'], true) && !empty($this->item->filesexist) && $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup') ) { diff --git a/source/packages/com_mokosuitebackup/tmpl/backup/default.php b/source/packages/com_mokosuitebackup/tmpl/backup/default.php index bcaf51f..49dfd58 100644 --- a/source/packages/com_mokosuitebackup/tmpl/backup/default.php +++ b/source/packages/com_mokosuitebackup/tmpl/backup/default.php @@ -30,6 +30,7 @@ $ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false) item->status) { 'complete' => 'badge bg-success', + 'warning' => 'badge bg-warning text-dark', 'running' => 'badge bg-info', 'fail' => 'badge bg-danger', default => 'badge bg-secondary', diff --git a/source/packages/com_mokosuitebackup/tmpl/backups/default.php b/source/packages/com_mokosuitebackup/tmpl/backups/default.php index f5e664a..5417461 100644 --- a/source/packages/com_mokosuitebackup/tmpl/backups/default.php +++ b/source/packages/com_mokosuitebackup/tmpl/backups/default.php @@ -92,6 +92,7 @@ $listDirn = $this->escape($this->state->get('list.direction')); status) { 'complete' => 'badge bg-success', + 'warning' => 'badge bg-warning text-dark', 'running' => 'badge bg-info', 'fail' => 'badge bg-danger', default => 'badge bg-secondary', diff --git a/source/packages/mod_mokosuitebackup_cpanel/language/en-GB/mod_mokosuitebackup_cpanel.ini b/source/packages/mod_mokosuitebackup_cpanel/language/en-GB/mod_mokosuitebackup_cpanel.ini index 50cb35a..6ca28a1 100644 --- a/source/packages/mod_mokosuitebackup_cpanel/language/en-GB/mod_mokosuitebackup_cpanel.ini +++ b/source/packages/mod_mokosuitebackup_cpanel/language/en-GB/mod_mokosuitebackup_cpanel.ini @@ -11,6 +11,7 @@ MOD_MOKOSUITEBACKUP_CPANEL_NOT_INSTALLED="MokoSuiteBackup is not installed or is MOD_MOKOSUITEBACKUP_CPANEL_LAST_BACKUP="Last Backup" MOD_MOKOSUITEBACKUP_CPANEL_STATUS_OK="Success" +MOD_MOKOSUITEBACKUP_CPANEL_STATUS_WARNING="Warning" MOD_MOKOSUITEBACKUP_CPANEL_STATUS_FAIL="Failed" MOD_MOKOSUITEBACKUP_CPANEL_NO_BACKUPS="No backups yet." MOD_MOKOSUITEBACKUP_CPANEL_FILES_TABLES="%d files, %d tables" diff --git a/source/packages/mod_mokosuitebackup_cpanel/tmpl/default.php b/source/packages/mod_mokosuitebackup_cpanel/tmpl/default.php index bad6adf..456d8fa 100644 --- a/source/packages/mod_mokosuitebackup_cpanel/tmpl/default.php +++ b/source/packages/mod_mokosuitebackup_cpanel/tmpl/default.php @@ -51,10 +51,20 @@ $moduleId = 'mod-msb-cpanel-' . $displayData['module']->id;
- - + 'bg-success', + 'warning' => 'bg-warning text-dark', + default => 'bg-danger', + }; + $cpanelLabel = match ($latest['status']) { + 'complete' => Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_OK'), + 'warning' => Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_WARNING'), + default => Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_FAIL'), + }; + ?> + +