Files
MokoSuiteBackup/source/packages/com_mokosuitebackup/tmpl/dashboard/default.php
T
jmiller 3742477aef
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 29s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
fix: convert inline modals to Bootstrap 5, fix language keys, ntfy default, and MokoRestore error handling
- Convert 10 inline CSS modals to Bootstrap 5 (backups: 7, snapshots: 3)
- Replace style.display show/hide with Bootstrap Modal API
- Fix JFIELD_ORDERING_LABEL_ASC → JFIELD_ORDERING_ASC in profile filter
- Add COM_MOKOJOOMBACKUP_CONFIGURATION key for Options page title
- Change ntfy default server to ntfy.mokoconsulting.tech
- Add profile ID to dropdown labels across backups, dashboard, cpanel module
- Add error handling to MokoRestore post() and runPreflight() to prevent UI stalling
- Remove outdated SSH auth pattern references from field descriptions
2026-06-25 08:35:40 -05:00

425 lines
16 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);
?>
<?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; ?>
<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); ?>
&mdash;
<?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')); ?>
&mdash;
<?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
&mdash; <?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="window.mokosuitebackupStart()">
<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>
<!-- Stepped Backup Modal (reused from backups view) -->
<div id="mokosuitebackup-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
<div style="max-width:500px; margin:10% auto; background:#fff; border-radius:8px; padding:2rem; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<h3 id="mb-modal-title" style="margin:0 0 1rem;">Backup in Progress</h3>
<div class="alert alert-warning py-1 px-2 mb-2" style="font-size:0.85rem;">
<span class="icon-warning-circle" aria-hidden="true"></span>
<strong>Do not navigate away or close this window</strong> while the backup is running.
</div>
<div style="background:#e9ecef; border-radius:4px; overflow:hidden; height:24px; margin-bottom:0.5rem;">
<div id="mb-progress-bar" style="height:100%; background:#0d6efd; transition:width 0.3s; width:0%; display:flex; align-items:center; justify-content:center; color:#fff; font-size:0.8rem; font-weight:bold;">0%</div>
</div>
<p id="mb-status" style="color:#666; font-size:0.9rem; margin:0.5rem 0;">Initializing...</p>
<p id="mb-phase" style="color:#999; font-size:0.8rem; margin:0;">Phase: init</p>
</div>
</div>
<script>
(function() {
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
var backupRunning = false;
window.addEventListener('beforeunload', function(e) {
if (backupRunning) { e.preventDefault(); e.returnValue = ''; }
});
function showModal() {
backupRunning = true;
document.getElementById('mokosuitebackup-modal').style.display = 'block';
}
function hideModal() {
backupRunning = false;
document.getElementById('mokosuitebackup-modal').style.display = 'none';
}
function updateProgress(progress, message, phase) {
const bar = document.getElementById('mb-progress-bar');
bar.style.width = progress + '%';
bar.textContent = progress + '%';
document.getElementById('mb-status').textContent = message;
document.getElementById('mb-phase').textContent = 'Phase: ' + phase;
}
async function postAjax(params) {
const form = new URLSearchParams();
form.append(TOKEN_NAME, '1');
for (const [k, v] of Object.entries(params)) {
form.append(k, v);
}
const res = await fetch(AJAX_URL, {
method: 'POST',
body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
return res.json();
}
async function startSteppedBackup() {
const profileSelect = document.getElementById('mb-profile-select');
const profileId = profileSelect ? profileSelect.value : '1';
showModal();
updateProgress(0, 'Initializing backup...', 'init');
try {
const initResult = await postAjax({
task: 'ajax.init',
profile_id: profileId
});
if (initResult.error) {
updateProgress(0, 'ERROR: ' + initResult.message, 'failed');
setTimeout(hideModal, 5000);
return;
}
// Show preflight warnings if any
if (initResult.warnings && initResult.warnings.length > 0) {
var warningEl = document.getElementById('mb-phase');
warningEl.textContent = 'Warnings: ' + initResult.warnings.join('; ');
warningEl.style.color = '#856404';
}
const sessionId = initResult.session_id;
updateProgress(initResult.progress, initResult.message, initResult.phase);
let done = false;
while (!done) {
const stepResult = await postAjax({
task: 'ajax.step',
session_id: sessionId
});
if (stepResult.error) {
updateProgress(0, 'ERROR: ' + stepResult.message, 'failed');
setTimeout(hideModal, 5000);
return;
}
updateProgress(stepResult.progress, stepResult.message, stepResult.phase);
done = stepResult.done || false;
}
document.getElementById('mb-modal-title').textContent = 'Backup Complete';
setTimeout(function() {
hideModal();
location.reload();
}, 2000);
} catch (err) {
updateProgress(0, 'ERROR: ' + err.message, 'failed');
setTimeout(hideModal, 5000);
}
}
window.mokosuitebackupStart = startSteppedBackup;
})();
</script>