Files
MokoSuiteBackup/source/packages/com_mokosuitebackup/tmpl/backups/default.php
T
jmiller 07fb4dcc24
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
fix: remove run/backup buttons, move actions to detail view, custom restore script name, version bump 01.43.11-dev
- Remove Run Backup / Backup Now buttons from profiles list, profile edit toolbar, and backup records view
- Move download, browse archive, and view log from backup list rows into individual backup record detail view
- Add download button to backup detail toolbar
- Link profile column in backup records list to profile edit
- Complete restore script filename customization across BackupEngine, SteppedBackupEngine, and MokoRestore
- Remove ordering field from profiles, default sort by ID ascending
- Fix untranslated JFIELD language keys
- Bump all manifests to 01.43.11-dev
2026-06-25 10:54:35 -05:00

784 lines
29 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\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
HTMLHelper::_('behavior.multiselect');
$user = Factory::getApplication()->getIdentity();
$canDownload = $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup');
$ajaxToken = Session::getFormToken();
$ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false);
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
?>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups'); ?>" method="post" name="adminForm" id="adminForm">
<div class="row">
<div class="col-md-12">
<div id="j-main-container" class="j-main-container">
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
<?php if (empty($this->items)) : ?>
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_NO_BACKUPS'); ?>
</div>
<?php else : ?>
<table class="table" id="backupList">
<caption class="visually-hidden"><?php echo Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TABLE_CAPTION'); ?></caption>
<thead>
<tr>
<td class="w-1 text-center">
<?php echo HTMLHelper::_('grid.checkall'); ?>
</td>
<th scope="col">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DESCRIPTION', 'a.description', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_PROFILE', 'a.profile_id', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_STATUS', 'a.status', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_TYPE', 'a.backup_type', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_SIZE', 'a.total_size', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DATE', 'a.backupstart', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-5">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $i => $item) : ?>
<tr>
<td class="text-center">
<?php echo HTMLHelper::_('grid.id', $i, $item->id); ?>
</td>
<td>
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backup&id=' . $item->id); ?>">
<?php echo $this->escape($item->description); ?>
</a>
<?php if (!empty($item->checksum)) : ?>
<br><small class="text-muted font-monospace"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_CHECKSUM'); ?>: <?php echo substr($item->checksum, 0, 16); ?>...</small>
<?php endif; ?>
</td>
<td>
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=profile.edit&id=' . (int) $item->profile_id); ?>">
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
</a>
</td>
<td>
<?php
$statusClass = match ($item->status) {
'complete' => 'badge bg-success',
'running' => 'badge bg-info',
'fail' => 'badge bg-danger',
default => 'badge bg-secondary',
};
?>
<span class="<?php echo $statusClass; ?>"><?php echo $this->escape($item->status); ?></span>
</td>
<td>
<?php echo $this->escape($item->backup_type); ?>
</td>
<td>
<?php
if ($item->total_size > 0) {
echo HTMLHelper::_('number.bytes', $item->total_size);
} else {
echo '—';
}
?>
</td>
<td>
<?php echo HTMLHelper::_('date', $item->backupstart, Text::_('DATE_FORMAT_LC4')); ?>
</td>
<td>
<?php echo (int) $item->id; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php echo $this->pagination->getListFooter(); ?>
<?php endif; ?>
<input type="hidden" name="task" value="">
<input type="hidden" name="boxchecked" value="0">
<?php echo HTMLHelper::_('form.token'); ?>
</div>
</div>
</div>
</form>
<!-- Stepped Backup Modal (for shared hosting) -->
<div class="modal fade" id="mokosuitebackup-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mb-modal-title">Backup in Progress</h5>
</div>
<div class="modal-body">
<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 class="progress mb-2" style="height:24px;">
<div id="mb-progress-bar" class="progress-bar" role="progressbar" style="width:0%;">0%</div>
</div>
<p id="mb-status" class="text-muted mb-1" style="font-size:0.9rem;">Initializing...</p>
<p id="mb-phase" class="text-muted mb-0" style="font-size:0.8rem;">Phase: init</p>
</div>
</div>
</div>
</div>
<script>
(function() {
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
var backupRunning = false;
function warnBeforeClose(e) {
if (backupRunning) {
e.preventDefault();
e.returnValue = '';
}
}
window.addEventListener('beforeunload', warnBeforeClose);
function showModal() {
backupRunning = true;
bootstrap.Modal.getOrCreateInstance(document.getElementById('mokosuitebackup-modal')).show();
}
function hideModal() {
backupRunning = false;
bootstrap.Modal.getInstance(document.getElementById('mokosuitebackup-modal'))?.hide();
}
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 {
// Init
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);
// Run steps until done
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;
}
// Complete
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);
}
}
// Expose for toolbar button
window.mokosuitebackupStart = startSteppedBackup;
// Intercept Restore toolbar button to show the modal
document.addEventListener('DOMContentLoaded', function() {
var restoreBtn = document.querySelector('[onclick*="backups.restore"], .button-upload');
if (restoreBtn) {
restoreBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Get selected record from checkboxes
var checked = document.querySelectorAll('input[name="cid[]"]:checked');
if (checked.length === 0) {
alert('<?php echo Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', true); ?>');
return false;
}
document.getElementById('mb-restore-record-id').value = checked[0].value;
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-restore-modal')).show();
return false;
}, true);
}
});
// Close restore modal handled by Bootstrap data-bs-dismiss
// AJAX stepped restore
var restoreRunning = false;
function showRestoreProgress() {
restoreRunning = true;
bootstrap.Modal.getInstance(document.getElementById('mb-restore-modal'))?.hide();
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-restore-progress-modal')).show();
}
function hideRestoreProgress() {
restoreRunning = false;
bootstrap.Modal.getInstance(document.getElementById('mb-restore-progress-modal'))?.hide();
}
function updateRestoreProgress(progress, message, phase) {
var bar = document.getElementById('mb-restore-progress-bar');
bar.style.width = progress + '%';
bar.textContent = progress + '%';
document.getElementById('mb-restore-status').textContent = message;
document.getElementById('mb-restore-phase').textContent = 'Phase: ' + phase;
}
window.addEventListener('beforeunload', function(e) {
if (restoreRunning) {
e.preventDefault();
e.returnValue = '';
}
});
async function startSteppedRestore(e) {
e.preventDefault();
var recordId = document.getElementById('mb-restore-record-id').value;
var restoreFiles = document.getElementById('mb-restore-files').checked ? 1 : 0;
var restoreDb = document.getElementById('mb-restore-db').checked ? 1 : 0;
var preserveConfig = document.getElementById('mb-restore-config').checked ? 1 : 0;
var password = document.getElementById('mb-restore-password').value;
showRestoreProgress();
updateRestoreProgress(0, 'Initializing restore...', 'init');
try {
var initResult = await postAjax({
task: 'ajax.restoreInit',
id: recordId,
restore_files: restoreFiles,
restore_db: restoreDb,
preserve_config: preserveConfig,
encryption_password: password
});
if (initResult.error) {
updateRestoreProgress(0, 'ERROR: ' + initResult.message, 'failed');
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
setTimeout(hideRestoreProgress, 5000);
return;
}
var sessionId = initResult.session_id;
updateRestoreProgress(initResult.progress, initResult.message, initResult.phase);
var done = false;
while (!done) {
var stepResult = await postAjax({
task: 'ajax.restoreStep',
session_id: sessionId
});
if (stepResult.error) {
updateRestoreProgress(0, 'ERROR: ' + stepResult.message, 'failed');
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
setTimeout(hideRestoreProgress, 5000);
return;
}
updateRestoreProgress(stepResult.progress, stepResult.message, stepResult.phase);
done = stepResult.done || false;
}
document.getElementById('mb-restore-title').textContent = 'Restore Complete';
setTimeout(function() {
hideRestoreProgress();
location.reload();
}, 2000);
} catch (err) {
updateRestoreProgress(0, 'ERROR: ' + err.message, 'failed');
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
setTimeout(hideRestoreProgress, 5000);
}
}
// Attach the AJAX restore handler to the restore form
document.addEventListener('DOMContentLoaded', function() {
var restoreForm = document.getElementById('mb-restore-form');
if (restoreForm) {
restoreForm.addEventListener('submit', startSteppedRestore);
}
});
})();
</script>
<!-- Restore Confirmation Modal -->
<div class="modal fade" id="mb-restore-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.restore'); ?>" method="post" id="mb-restore-form">
<input type="hidden" name="id" id="mb-restore-record-id" value="">
<div class="modal-body">
<div class="alert alert-danger">
<span class="icon-warning-circle" aria-hidden="true"></span>
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_CONFIRM'); ?></strong>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="restore_files" value="1" id="mb-restore-files" checked>
<label class="form-check-label" for="mb-restore-files">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_FILES'); ?>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="restore_db" value="1" id="mb-restore-db" checked>
<label class="form-check-label" for="mb-restore-db">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_DATABASE'); ?>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="preserve_config" value="1" id="mb-restore-config" checked>
<label class="form-check-label" for="mb-restore-config">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG'); ?>
<small class="text-muted d-block"><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG_DESC'); ?></small>
</label>
</div>
</div>
<div class="mb-3">
<label for="mb-restore-password" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD'); ?></label>
<input type="password" class="form-control" id="mb-restore-password" name="encryption_password"
placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PASSWORD_PLACEHOLDER'); ?>" autocomplete="off">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-danger">
<span class="icon-upload" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
</div>
<!-- Restore Progress Modal -->
<div class="modal fade" id="mb-restore-progress-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mb-restore-title">Restore in Progress</h5>
</div>
<div class="modal-body">
<div class="progress mb-2" style="height:24px;">
<div id="mb-restore-progress-bar" class="progress-bar bg-danger" role="progressbar" style="width:0%;">0%</div>
</div>
<p id="mb-restore-status" class="text-muted mb-1" style="font-size:0.9rem;">Initializing...</p>
<p id="mb-restore-phase" class="text-muted mb-0" style="font-size:0.8rem;">Phase: init</p>
</div>
</div>
</div>
</div>
<!-- Purge Backups Modal -->
<?php $canDelete = $user->authorise('core.delete', 'com_mokosuitebackup'); ?>
<?php if ($canDelete) : ?>
<div class="modal fade" id="mb-purge-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<span class="icon-trash" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_TITLE'); ?>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.purge'); ?>" method="post" id="mb-purge-form">
<div class="modal-body">
<p><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DESC'); ?></p>
<div class="mb-3">
<label for="mb-purge-date" class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DATE_LABEL'); ?></label>
<input type="date" class="form-control" id="mb-purge-date" name="purge_date" required>
</div>
<div id="mb-purge-count-wrapper" style="display:none;">
<div class="alert alert-danger mb-0" id="mb-purge-count-msg"></div>
</div>
<div id="mb-purge-none-wrapper" style="display:none;">
<div class="alert alert-info mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND'); ?></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-danger" id="mb-purge-submit" disabled>
<span class="icon-trash" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_SUBMIT'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
</div>
</div>
<?php endif; ?>
<!-- Backup Comparison Modal -->
<div class="modal fade" id="mb-compare-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<span class="icon-copy" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_TITLE'); ?>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="max-height:65vh; overflow-y:auto;">
<div id="mb-compare-loading" class="text-center py-4">
<span class="icon-spinner icon-spin" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_LOADING'); ?>
</div>
<div id="mb-compare-error" style="display:none;" class="alert alert-danger"></div>
<table id="mb-compare-table" class="table table-striped" style="display:none;">
<thead>
<tr>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_FIELD'); ?></th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_BACKUP'); ?> 1</th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_BACKUP'); ?> 2</th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_DELTA'); ?></th>
</tr>
</thead>
<tbody id="mb-compare-body"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
(function() {
var COMPARE_AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
var COMPARE_TOKEN = <?php echo json_encode($ajaxToken); ?>;
function mbCmpFormatBytes(bytes) {
if (bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024));
if (i >= units.length) i = units.length - 1;
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
}
function mbCmpFormatDuration(seconds) {
if (seconds <= 0) return '0s';
var m = Math.floor(seconds / 60);
var s = seconds % 60;
return m > 0 ? m + 'm ' + s + 's' : s + 's';
}
function mbCmpDeltaCell(value, unit) {
if (value === 0) return '<span class="text-muted">&mdash;</span>';
var isPositive = value > 0;
var colorClass = isPositive ? 'text-danger' : 'text-success';
var display;
if (unit === 'bytes') {
display = (isPositive ? '+' : '') + mbCmpFormatBytes(value);
} else if (unit === 'duration') {
display = (isPositive ? '+' : '-') + mbCmpFormatDuration(Math.abs(value));
} else {
display = (isPositive ? '+' : '') + value.toLocaleString();
}
return '<span class="fw-bold ' + colorClass + '">' + display + '</span>';
}
function mbShowCompareModal(id1, id2) {
var modal = document.getElementById('mb-compare-modal');
var loading = document.getElementById('mb-compare-loading');
var errorEl = document.getElementById('mb-compare-error');
var table = document.getElementById('mb-compare-table');
var body = document.getElementById('mb-compare-body');
bootstrap.Modal.getOrCreateInstance(modal).show();
loading.style.display = 'block';
errorEl.style.display = 'none';
table.style.display = 'none';
body.innerHTML = '';
var form = new URLSearchParams();
form.append('task', 'ajax.compareBackups');
form.append('id1', id1);
form.append('id2', id2);
form.append(COMPARE_TOKEN, '1');
fetch(COMPARE_AJAX_URL, {
method: 'POST',
body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) { return r.json(); })
.then(function(data) {
loading.style.display = 'none';
if (data.error) {
errorEl.textContent = data.message || 'Error loading comparison';
errorEl.style.display = 'block';
return;
}
var b1 = data.backup1;
var b2 = data.backup2;
var d = data.delta;
var dur1 = 0, dur2 = 0;
if (b1.backupstart !== '0000-00-00 00:00:00' && b1.backupend !== '0000-00-00 00:00:00') {
dur1 = (new Date(b1.backupend).getTime() - new Date(b1.backupstart).getTime()) / 1000;
}
if (b2.backupstart !== '0000-00-00 00:00:00' && b2.backupend !== '0000-00-00 00:00:00') {
dur2 = (new Date(b2.backupend).getTime() - new Date(b2.backupstart).getTime()) / 1000;
}
var rows = [
['<?php echo Text::_('JGRID_HEADING_ID', true); ?>', '#' + b1.id, '#' + b2.id, ''],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_DESCRIPTION', true); ?>', b1.description || '&mdash;', b2.description || '&mdash;', ''],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_PROFILE', true); ?>', b1.profile_title || '&mdash;', b2.profile_title || '&mdash;', ''],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_STATUS', true); ?>', b1.status, b2.status, ''],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_TYPE', true); ?>', b1.backup_type, b2.backup_type, ''],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_SIZE', true); ?>', mbCmpFormatBytes(b1.total_size), mbCmpFormatBytes(b2.total_size), mbCmpDeltaCell(d.size_diff, 'bytes')],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_DB_SIZE', true); ?>', mbCmpFormatBytes(b1.db_size), mbCmpFormatBytes(b2.db_size), mbCmpDeltaCell(b2.db_size - b1.db_size, 'bytes')],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_FILES_COUNT', true); ?>', b1.files_count.toLocaleString(), b2.files_count.toLocaleString(), mbCmpDeltaCell(d.files_diff, 'number')],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_TABLES_COUNT', true); ?>', b1.tables_count.toLocaleString(), b2.tables_count.toLocaleString(), mbCmpDeltaCell(d.tables_diff, 'number')],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_DATE', true); ?>', b1.backupstart, b2.backupstart, ''],
['<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_DURATION', true); ?>', mbCmpFormatDuration(dur1), mbCmpFormatDuration(dur2), mbCmpDeltaCell(d.duration_diff_seconds, 'duration')],
];
var html = '';
for (var i = 0; i < rows.length; i++) {
html += '<tr><td class="fw-bold">' + rows[i][0] + '</td><td>' + rows[i][1] + '</td><td>' + rows[i][2] + '</td><td>' + rows[i][3] + '</td></tr>';
}
body.innerHTML = html;
table.style.display = 'table';
})
.catch(function(err) {
loading.style.display = 'none';
errorEl.textContent = 'Error: ' + err.message;
errorEl.style.display = 'block';
});
}
// Compare modal close handled by Bootstrap data-bs-dismiss
// Intercept Compare toolbar button
document.addEventListener('DOMContentLoaded', function() {
var compareBtn = document.querySelector('[onclick*="backups.compare"], .button-copy');
if (compareBtn) {
compareBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
var checked = document.querySelectorAll('input[name="cid[]"]:checked');
if (checked.length !== 2) {
alert('<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_SELECT_TWO', true); ?>');
return false;
}
mbShowCompareModal(checked[0].value, checked[1].value);
return false;
}, true);
}
});
})();
</script>
<?php if ($canDelete) : ?>
<script>
(function() {
var PURGE_AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
var PURGE_TOKEN = <?php echo json_encode($ajaxToken); ?>;
var purgeCountTimer = null;
// Intercept Purge toolbar button to show the modal
document.addEventListener('DOMContentLoaded', function() {
var purgeBtn = document.querySelector('[onclick*="backups.purgeModal"], .button-trash');
if (purgeBtn) {
purgeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Reset modal state
document.getElementById('mb-purge-date').value = '';
document.getElementById('mb-purge-count-wrapper').style.display = 'none';
document.getElementById('mb-purge-none-wrapper').style.display = 'none';
document.getElementById('mb-purge-submit').disabled = true;
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-purge-modal')).show();
return false;
}, true);
}
// Date change triggers count lookup with debounce
var dateInput = document.getElementById('mb-purge-date');
if (dateInput) {
dateInput.addEventListener('change', function() {
if (purgeCountTimer) clearTimeout(purgeCountTimer);
purgeCountTimer = setTimeout(fetchPurgeCount, 300);
});
}
// Purge modal close handled by Bootstrap data-bs-dismiss
// Confirm on submit
var purgeForm = document.getElementById('mb-purge-form');
if (purgeForm) {
purgeForm.addEventListener('submit', function(e) {
var msg = document.getElementById('mb-purge-count-msg').textContent;
if (!confirm(msg + '\n\n<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_CONFIRM', true); ?>')) {
e.preventDefault();
}
});
}
});
function fetchPurgeCount() {
var dateVal = document.getElementById('mb-purge-date').value;
var countWrapper = document.getElementById('mb-purge-count-wrapper');
var noneWrapper = document.getElementById('mb-purge-none-wrapper');
var countMsg = document.getElementById('mb-purge-count-msg');
var submitBtn = document.getElementById('mb-purge-submit');
if (!dateVal) {
countWrapper.style.display = 'none';
noneWrapper.style.display = 'none';
submitBtn.disabled = true;
return;
}
countMsg.textContent = '<?php echo Text::_('COM_MOKOJOOMBACKUP_LOADING', true); ?>';
countWrapper.style.display = 'block';
noneWrapper.style.display = 'none';
submitBtn.disabled = true;
var form = new URLSearchParams();
form.append('task', 'ajax.countPurge');
form.append('date', dateVal);
form.append(PURGE_TOKEN, '1');
fetch(PURGE_AJAX_URL, {
method: 'POST',
body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
countMsg.textContent = data.message || 'Error';
countWrapper.style.display = 'block';
noneWrapper.style.display = 'none';
submitBtn.disabled = true;
} else if (data.count === 0) {
countWrapper.style.display = 'none';
noneWrapper.style.display = 'block';
submitBtn.disabled = true;
} else {
var text = '<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_COUNT_MSG', true); ?>';
countMsg.textContent = text.replace('%d', data.count);
countWrapper.style.display = 'block';
noneWrapper.style.display = 'none';
submitBtn.disabled = false;
}
})
.catch(function(err) {
countMsg.textContent = 'Error: ' + err.message;
countWrapper.style.display = 'block';
noneWrapper.style.display = 'none';
submitBtn.disabled = true;
});
}
})();
</script>
<?php endif; ?>