8cfa46d985
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 12s
Universal: PR Check / Validate PR (pull_request) Failing after 13s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 49s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Replace the popup-modal pre-update approach with a dedicated full-page backup screen, modelled on Akeeba Backup-on-Update, and fix the white screen caused by running a backup synchronously inside the update request. - New view=runbackup (View/Runbackup + tmpl/runbackup): full-screen progress page that auto-starts the stepped backup (ajax.init -> loop ajax.step), then redirects to a validated returnurl on completion or shows a completion/retry panel. Auto-resolved by MVCFactory. - System plugin: on com_joomlaupdate task=update.install (when backup_before_update is on), redirect to the full-screen backup screen with returnurl back to update.install&is_backed_up=1; on return, arm the throttle so onExtensionBeforeUpdate does not run a duplicate backup. No synchronous backup in the update request. - Dashboard 'Backup Now' navigates to the full-screen screen instead of the inline modal; the inline modal + stepped JS are removed. - Retire the popup modal: delete media/js/update-backup.js, remove the plugin <media> element and the preupdate_auto_intercept param. - Dashboard: warn when Global Config live_site is empty (CLI/scheduled backup naming + URL generation depend on it). Refs #196 Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
333 lines
14 KiB
PHP
333 lines
14 KiB
PHP
<?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
|
|
*/
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\HTML\HTMLHelper;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Session\Session;
|
|
|
|
$ajaxToken = Session::getFormToken();
|
|
$ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false);
|
|
|
|
$runbackupUrl = Route::_('index.php?option=com_mokosuitebackup&view=runbackup&autostart=1', false);
|
|
$liveSite = trim((string) \Joomla\CMS\Factory::getApplication()->get('live_site', ''));
|
|
?>
|
|
<?php if ($this->defaultDirWarning) : ?>
|
|
<div class="alert alert-warning d-flex align-items-center mb-3" role="alert">
|
|
<span class="icon-warning-circle fs-4 me-3" aria-hidden="true"></span>
|
|
<div>
|
|
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE'); ?></strong><br>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING'); ?>
|
|
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=profiles'); ?>" class="alert-link">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_SUBMENU_PROFILES'); ?>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($liveSite === '') : ?>
|
|
<div class="alert alert-warning d-flex align-items-center mb-3" role="alert">
|
|
<span class="icon-warning-circle fs-4 me-3" aria-hidden="true"></span>
|
|
<div>
|
|
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_LIVESITE_WARNING_TITLE'); ?></strong><br>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_LIVESITE_WARNING'); ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="row">
|
|
<!-- Row 1: Status Cards (clickable) -->
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card h-100 mb-tile" role="link" data-href="<?php echo $this->lastBackup ? Route::_('index.php?option=com_mokosuitebackup&view=backup&id=' . $this->lastBackup->id) : Route::_('index.php?option=com_mokosuitebackup&view=backups'); ?>">
|
|
<div class="card-body text-center">
|
|
<span class="icon-database fs-1 text-primary" aria-hidden="true"></span>
|
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP'); ?></h5>
|
|
<?php if ($this->lastBackup) : ?>
|
|
<p class="card-text text-success fw-bold">
|
|
<?php echo HTMLHelper::_('date', $this->lastBackup->backupend, Text::_('DATE_FORMAT_LC4')); ?>
|
|
</p>
|
|
<small class="text-muted">
|
|
<?php echo $this->escape($this->lastBackup->profile_title); ?>
|
|
—
|
|
<?php echo HTMLHelper::_('number.bytes', $this->lastBackup->total_size); ?>
|
|
</small>
|
|
<?php else : ?>
|
|
<p class="card-text text-warning"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS'); ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>">
|
|
<div class="card-body text-center">
|
|
<span class="icon-calendar fs-1 text-info" aria-hidden="true"></span>
|
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED'); ?></h5>
|
|
<?php if ($this->nextScheduled) : ?>
|
|
<p class="card-text fw-bold">
|
|
<?php echo HTMLHelper::_('date', $this->nextScheduled->next_execution, Text::_('DATE_FORMAT_LC4')); ?>
|
|
</p>
|
|
<small class="text-muted"><?php echo $this->escape($this->nextScheduled->title); ?></small>
|
|
<?php else : ?>
|
|
<p class="card-text text-muted"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_SCHEDULED'); ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups'); ?>">
|
|
<div class="card-body text-center">
|
|
<span class="icon-copy fs-1 text-secondary" aria-hidden="true"></span>
|
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_TOTAL_BACKUPS'); ?></h5>
|
|
<p class="card-text fw-bold fs-3"><?php echo (int) $this->stats->total_count; ?></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups'); ?>">
|
|
<div class="card-body text-center">
|
|
<span class="icon-folder-open fs-1 text-warning" aria-hidden="true"></span>
|
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE'); ?></h5>
|
|
<p class="card-text fw-bold fs-3">
|
|
<?php echo HTMLHelper::_('number.bytes', (int) $this->stats->total_size); ?>
|
|
</p>
|
|
<?php if ($this->stats->fail_count_7d > 0) : ?>
|
|
<span class="badge bg-danger">
|
|
<?php echo Text::sprintf('COM_MOKOJOOMBACKUP_DASHBOARD_FAILURES_7D', $this->stats->fail_count_7d); ?>
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.mb-tile { cursor: pointer; transition: box-shadow 0.2s, transform 0.1s; }
|
|
.mb-tile:hover { box-shadow: 0 .5rem 1rem rgba(0,0,0,.15); transform: translateY(-2px); }
|
|
</style>
|
|
<script>
|
|
document.querySelectorAll('.mb-tile').forEach(function(tile) {
|
|
tile.addEventListener('click', function() { window.location.href = this.dataset.href; });
|
|
});
|
|
</script>
|
|
|
|
<!-- Row 1b: Snapshot Widget -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<div class="card h-100">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title mb-0">
|
|
<span class="icon-camera" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_SNAPSHOTS'); ?>
|
|
</h5>
|
|
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=snapshots'); ?>" class="btn btn-sm btn-outline-secondary">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_VIEW_ALL'); ?>
|
|
</a>
|
|
</div>
|
|
<div class="card-body">
|
|
<?php if ($this->latestSnapshot) : ?>
|
|
<?php $types = json_decode($this->latestSnapshot->content_types, true) ?: []; ?>
|
|
<p class="mb-1">
|
|
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_LATEST_SNAPSHOT'); ?>:</strong>
|
|
<?php echo $this->escape($this->latestSnapshot->description); ?>
|
|
</p>
|
|
<p class="mb-1 text-muted">
|
|
<?php echo HTMLHelper::_('date', $this->latestSnapshot->created, Text::_('DATE_FORMAT_LC4')); ?>
|
|
—
|
|
<?php foreach ($types as $type) : ?>
|
|
<span class="badge bg-secondary"><?php echo $this->escape($type); ?></span>
|
|
<?php endforeach; ?>
|
|
</p>
|
|
<p class="mb-0">
|
|
<small class="text-muted">
|
|
<?php echo (int) $this->latestSnapshot->articles_count; ?> articles,
|
|
<?php echo (int) $this->latestSnapshot->categories_count; ?> categories,
|
|
<?php echo (int) $this->latestSnapshot->modules_count; ?> modules
|
|
— <?php echo $this->snapshotCount; ?> total snapshots
|
|
</small>
|
|
</p>
|
|
<?php else : ?>
|
|
<p class="text-muted mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_SNAPSHOTS'); ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Storage Breakdown by Profile -->
|
|
<div class="col-md-6">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<span class="icon-folder-open" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE_BREAKDOWN'); ?>
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<?php if (!empty($this->storageByProfile)) : ?>
|
|
<?php
|
|
$maxSize = max(array_column($this->storageByProfile, 'total_size')) ?: 1;
|
|
$colors = ['#0d6efd', '#198754', '#ffc107', '#dc3545', '#6f42c1', '#0dcaf0'];
|
|
?>
|
|
<?php foreach ($this->storageByProfile as $i => $profile) : ?>
|
|
<?php $pct = round(($profile->total_size / $maxSize) * 100); ?>
|
|
<div class="mb-2">
|
|
<div class="d-flex justify-content-between small">
|
|
<span><?php echo $this->escape($profile->profile_title ?: 'Unknown'); ?> (<?php echo (int) $profile->backup_count; ?>)</span>
|
|
<span><?php echo HTMLHelper::_('number.bytes', $profile->total_size); ?></span>
|
|
</div>
|
|
<div style="background:#e9ecef; border-radius:3px; height:8px; overflow:hidden;">
|
|
<div style="width:<?php echo $pct; ?>%; height:100%; background:<?php echo $colors[$i % count($colors)]; ?>; border-radius:3px;"></div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php else : ?>
|
|
<p class="text-muted mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS'); ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup Trend (30 days) -->
|
|
<?php if (!empty($this->backupTrend)) : ?>
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<span class="icon-chart" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND'); ?>
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<?php
|
|
$maxDaySize = max(array_column($this->backupTrend, 'day_size')) ?: 1;
|
|
?>
|
|
<div style="display:flex; align-items:flex-end; gap:2px; height:120px; overflow-x:auto;">
|
|
<?php foreach ($this->backupTrend as $day) : ?>
|
|
<?php
|
|
$barHeight = max(4, round(($day->day_size / $maxDaySize) * 100));
|
|
$barColor = $day->fail_count > 0 ? '#dc3545' : '#198754';
|
|
$tooltip = date('M j', strtotime($day->backup_date))
|
|
. ' — ' . $day->day_count . ' backup(s), '
|
|
. number_format($day->day_size / 1048576, 1) . ' MB'
|
|
. ($day->fail_count > 0 ? ', ' . $day->fail_count . ' failed' : '');
|
|
?>
|
|
<div style="flex:1; min-width:8px; max-width:24px; height:<?php echo $barHeight; ?>%; background:<?php echo $barColor; ?>; border-radius:2px 2px 0 0; cursor:default;"
|
|
title="<?php echo htmlspecialchars($tooltip); ?>"></div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="d-flex justify-content-between mt-1">
|
|
<small class="text-muted"><?php echo date('M j', strtotime('-30 days')); ?></small>
|
|
<small class="text-muted"><?php echo date('M j'); ?></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Row 2: Quick Actions -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_QUICK_ACTIONS'); ?></h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<?php if (!empty($this->profiles)) : ?>
|
|
<div class="mb-3">
|
|
<select id="mb-profile-select" class="form-select mb-2">
|
|
<?php foreach ($this->profiles as $profile) : ?>
|
|
<option value="<?php echo (int) $profile->id; ?>">
|
|
#<?php echo (int) $profile->id; ?> —
|
|
<?php echo $this->escape($profile->title); ?>
|
|
(<?php echo $this->escape($profile->backup_type); ?>)
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<button type="button" class="btn btn-primary w-100" onclick="mokosuitebackupGo()">
|
|
<span class="icon-download" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW'); ?>
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="list-group">
|
|
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups'); ?>" class="list-group-item list-group-item-action">
|
|
<span class="icon-database" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS'); ?>
|
|
</a>
|
|
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=profiles'); ?>" class="list-group-item list-group-item-action">
|
|
<span class="icon-cog" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_SUBMENU_PROFILES'); ?>
|
|
</a>
|
|
<a href="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>" class="list-group-item list-group-item-action">
|
|
<span class="icon-calendar" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_SCHEDULED_TASKS'); ?>
|
|
</a>
|
|
<a href="<?php echo Route::_('index.php?option=com_installer&view=updatesites'); ?>" class="list-group-item list-group-item-action">
|
|
<span class="icon-refresh" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE'); ?>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 2 right: System Health -->
|
|
<div class="col-md-6">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH'); ?></h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<table class="table table-sm mb-0">
|
|
<tbody>
|
|
<?php foreach ($this->systemHealth as $check) : ?>
|
|
<tr>
|
|
<td class="w-1 text-center">
|
|
<?php if ($check->status) : ?>
|
|
<span class="icon-publish text-success" aria-hidden="true"></span>
|
|
<?php else : ?>
|
|
<span class="icon-unpublish text-danger" aria-hidden="true"></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php echo $this->escape($check->label); ?></td>
|
|
<td class="text-muted"><?php echo $this->escape($check->detail); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
/* "Backup Now" navigates to the full-screen backup progress page (view=runbackup)
|
|
for the selected profile — the same screen used for the pre-update backup. */
|
|
(function () {
|
|
'use strict';
|
|
|
|
var RUNBACKUP_URL = <?php echo json_encode($runbackupUrl); ?>;
|
|
|
|
window.mokosuitebackupGo = function () {
|
|
var sel = document.getElementById('mb-profile-select');
|
|
var pid = sel ? sel.value : '1';
|
|
window.location.href = RUNBACKUP_URL + '&profile_id=' + encodeURIComponent(pid);
|
|
};
|
|
})();
|
|
</script>
|