Files
openpanel/templates/tabler/backups.html
2024-10-25 01:44:26 +02:00

2963 lines
155 KiB
HTML

{% extends 'base.html' %}
{% block content %}
<!-- Delete JOB Modal -->
<div class="modal modal-blur fade" id="confirmDeleteJobModal" aria-labelledby="confirmDeleteJobModalLabel" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<button type="button" id="deleteJobModalxClose" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-status bg-danger"></div>
<div class="modal-body text-center py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
<h3 id="confirmDeleteJobModalLabel">Are you sure you want to delete the backup job?</h3>
<div><span class="text-muted">ID: <span id="job_to_delete"></span></div>
<div><span class="text-muted">Name: <span id="job_to_delete_name"></span></div>
<div class="mt-3 mb-3">
<label for="deleteJobConfirmation" class="form-label">Type "DELETE" to confirm:</label>
<input type="text" class="form-control" id="deleteJobConfirmation">
</div>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col"><a href="#" id="confirmJobDelete" class="btn btn-danger w-100" style="display: none;">
Delete
</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Destination Modal -->
<div class="modal modal-blur fade" id="confirmDeleteDestinationModal" aria-labelledby="confirmDeleteDestinationModalLabel" tabindex="-1" role="dialog" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<button type="button" id="deleteModalxClose" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-status bg-danger"></div>
<div class="modal-body text-center py-4">
<!-- Download SVG icon from http://tabler-icons.io/i/alert-triangle -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
<h3 id="confirmDeleteDestinationModalLabel">Are you sure you want to delete the destination?</h3>
<div><span class="text-muted">Destination ID: <span id="destination_to_delete"></span></div>
<div><span class="text-muted">Destination Hostname: <span id="destination_to_delete_name"></span></div>
<!-- Input field for confirmation -->
<div class="mt-3 mb-3">
<label for="deleteConfirmation" class="form-label">Type "DELETE" to confirm:</label>
<input type="text" class="form-control" id="deleteConfirmation">
</div>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<!--div class="col"><a href="#" class="btn w-100" data-bs-dismiss="modal">
Cancel
</a></div-->
<div class="col"><a href="#" id="confirmDelete" class="btn btn-danger w-100" style="display: none;">
Delete
</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- create backup job modal -->
<div class="modal fade" id="createJobModal" tabindex="-1" aria-labelledby="createJobModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createJobModalLabel">Add a New Backup Job</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Input fields for editing destination data -->
<form id="createForm">
<label class="form-label">Backup type</label>
<div class="form-selectgroup-boxes row mb-3">
<div class="col-lg-4">
<label class="form-selectgroup-item">
<input type="radio" name="job-type" value="accounts" class="form-selectgroup-input" checked="">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Account Snapshots</span>
<span class="d-block text-secondary">Create exact point-in-time backup of users docker container.</span>
</span>
</span>
</label>
</div>
<div class="col-lg-4">
<label class="form-selectgroup-item">
<input type="radio" name="job-type" value="partial" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Partial Backups</span>
<span class="d-block text-secondary">Backup only selected account data (files, databases, crons, etc.)</span>
</span>
</span>
</label>
</div>
<div class="col-lg-4">
<label class="form-selectgroup-item">
<input type="radio" name="job-type" value="configuration" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Server Configuration</span>
<span class="d-block text-secondary">Backup OpenPanel server and services configuration.</span>
</span>
</span>
</label>
</div>
</div>
<!-- Add this script at the end of your HTML, after the modal content -->
<script>
document.addEventListener('DOMContentLoaded', function () {
var radioButtons = document.querySelectorAll('input[name="job-type"]');
var accountsOnlyElement = document.getElementById('for_accounts_only');
var accountsOnlyFullElement = document.getElementById('for_fullaccounts_only');
var configOnlyElement = document.getElementById('for_config_only');
function toggleElementVisibility() {
if (radioButtons[2].checked) {
// 'Configuration' is at index 2
accountsOnlyElement.style.display = 'none';
accountsOnlyFullElement.style.display = 'none';
configOnlyElement.style.display = 'flex';
} else if (radioButtons[1].checked) {
// 'Partial Backups' is at index 1
accountsOnlyElement.style.display = 'flex';
accountsOnlyFullElement.style.display = 'none';
configOnlyElement.style.display = 'none';
} else {
// 'Accounts' is at index 0
accountsOnlyElement.style.display = 'none';
accountsOnlyFullElement.style.display = 'flex';
configOnlyElement.style.display = 'none';
}
}
// Initial state
toggleElementVisibility();
// Add event listener to each radio button
radioButtons.forEach(function (radio) {
radio.addEventListener('change', toggleElementVisibility);
});
});
</script>
<div class="row" id="for_config_only">
<label class="form-label">Backup job contains:</label>
<div class="mb-3">
<label class="form-check">
<span class="form-check-label">
All OpenPanel Configuration and Users Information
</span>
<span class="form-check-description">
Contains all of your OpenAdmin settings, users and their information (domains, websites, information, preferences, etc.) —basically, all of the data required to move or restore an OpenPanel server, minus the user data itself.<br>
Also, you may copy all your OpenAdmin settings to another server using this data.
</span>
</label>
</div>
</div>
<div class="row" id="for_fullaccounts_only">
<label class="form-label">Backup job contains:</label>
<div class="mb-3">
<label class="form-check">
<span class="form-check-label">
Docker container and files
</span>
<span class="form-check-description">
Contains users docker container, files from the home directory and openpanel settings.<br>
This backup takes more space but has all users files, so its usefull in disaster recovery or for migrating accounts to another server.
</span>
</label>
</div>
</div>
<div class="row" id="for_accounts_only">
<label class="form-label">Backup job contains:</label>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
Panel Config
</span>
<span class="form-check-description">
Files needed for restoring user account in OpenPanel (email, list of domains, password, plan, etc.)
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
Databases
</span>
<span class="form-check-description">
MySQL Databases, Users, passwords and their privilegies.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
Services
</span>
<span class="form-check-description">
List of installed services, their versions and current status.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
Service Configuration
</span>
<span class="form-check-description">
Apache/Nginx, MySQL, TimeZone settings, etc.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
Cron Jobs
</span>
</label>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
Home Dir Files
</span>
<span class="form-check-description">
All files inside users home directory.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
Domains
</span>
<span class="form-check-description">
Domain names, Redirects, DNS Zones, Firewall rules, IP Blocks, etc.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
PHP Settings
</span>
<span class="form-check-description">
Installed PHP versions, default version, PHP.INI files.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
SSH Users
</span>
<span class="form-check-description">
List of all SSH users and their passwords.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked="">
<span class="form-check-label">
SSL Certificates
</span>
<span class="form-check-description">
SSL Certificate files: public keys and private keys.
</span>
</label>
</div>
</div>
</div>
</hr>
<div class="row">
<div class="col-lg-6">
<div class="mb-3">
<label for="create-job-name" class="form-label">Name</label>
<input type="text" class="form-control" id="create-job-name">
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label for="create-job-select-destination" class="form-label">Select Destination</label>
<select class="form-select" id="backupSelect"></select>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="mb-3">
<label for="create-job-path" id="directory_to_check" class="form-label">Directory</label>
<input type="text" class="form-control" id="create-job-path">
<div id="create-job-path-feedback" class="invalid-feedback">Directory does not exist on the local server.</div>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3" id="create-job-type-column">
<label for="create-job-type" class="form-label">Type</label>
<input type="text" class="form-control" id="create-job-type">
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="mb-3">
<label for="create-job-schedule" class="form-label">Schedule</label>
<select class="form-select" id="create-job-schedule">
<option label="Hourly" value="hourly">Hourly</option>
<option label="Daily" value="daily" selected="selected">Daily</option>
<option label="Weekly" value="weekly">Weekly</option>
<option label="Monthly" value="monthly">Monthly</option>
</select>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label for="create-job-retention" class="form-label">Retention</label>
<input type="number" class="form-control" id="create-job-retention" min="1" max="10000">
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="mb-3">
<label for="create-job-enable" class="form-label">Enable</label>
<select class="form-select" id="create-job-enable">
<option label="Yes" value="on" selected="selected">Yes</option>
<option label="No" value="off">No</option>
</select>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label for="create-job-filters" class="form-label">Filters</label>
<input type="text" class="form-control" id="create-job-filters">
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="submitJob()" id="addJobBtn">Add Backup Job</button>
</div>
</div>
</div>
</div>
<script>
// Function to handle saving new job in the modal
function submitJob() {
// Get data from input fields
var name = $('#create-job-name').val();
//var destination = $('#create-job-select-destination').val();
var destination = $('#backupSelect').val();
var directory = $('#create-job-path').val();
var type = $('input[name="job-type"]:checked').val();
var schedule = $('#create-job-schedule').val();
var retention = $('#create-job-retention').val();
var status = $('#create-job-enable').val();
var filters = $('#create-job-filters').val();
// Prepare data to send to the server
var postData = {
name: name,
destination: destination,
directory: directory,
type: type,
schedule: schedule,
retention: retention,
status: status,
filters: filters
};
// Make an Ajax request to post the job data
$.ajax({
url: '/backups/jobs',
type: 'POST',
data: postData,
success: function(response) {
// Handle the server response (success or error)
if (response.status === 'success') {
//close modal
$('#createJobModal').modal('hide');
//refresh table data immediately
updateJobsTable();
// Show success message
var successMessage = $('<div class="alert alert-success alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div><div>New backup job added successfully!</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
successMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
successMessage.fadeOut(500, function() {
successMessage.remove();
});
}, 5000);
} else if (response.status === 'failure') {
// Show failure message
showCreateErrorMessage('Error creating new backup job: ' + response.result);
} else {
// Show error message
showCreateErrorMessage('Error creating new backup job: ' + response.result);
}
},
error: function(error) {
console.error('Error creating backup job:', error);
// Show error message
showCreateErrorMessage('Error saving changes. Please try again.');
}
});
}
document.addEventListener('DOMContentLoaded', function () {
// Get the input element, the select element, and the button
var createJobPathInput = document.getElementById('create-job-path');
var backupSelect = document.getElementById('backupSelect');
var createJobPathFeedback = document.getElementById('create-job-path-feedback');
var addDestinationBtn = document.getElementById('addDestinationBtn');
// Add event listener on backupSelect change
backupSelect.addEventListener('change', function () {
// Check if the selected text in the dropdown is one of the specified values
var selectedText = backupSelect.options[backupSelect.selectedIndex].text;
var selectedTextLowercase = selectedText.toLowerCase();
var validValues = ['localhost', '127.0.0.1', '{{ force_domain_value }}', '{{ server_hostname }}', '{{ public_ip }}'];
var validValuesLowercase = validValues.map(value => value.toLowerCase());
// Check if the selected text is one of the valid values
if (validValuesLowercase.includes(selectedTextLowercase)) {
// Add the input event listener
createJobPathInput.addEventListener('input', inputEventListener);
} else {
// If the selected text is not in the valid list, remove both classes and enable the button
createJobPathInput.classList.remove('is-valid', 'is-invalid');
// Enable the button
addDestinationBtn.removeAttribute('disabled');
// Remove the input event listener
createJobPathInput.removeEventListener('input', inputEventListener);
}
});
// Input event listener
function inputEventListener() {
// Get the input value
var inputValue = createJobPathInput.value;
// Call the fetch function
fetchData(inputValue);
}
// Fetch data function
function fetchData(inputValue) {
// Make an asynchronous request to the Flask route
fetch('/backups/helpers/path?dir=' + inputValue)
.then(response => response.json())
.then(data => {
if (data.exists) {
// If it exists, add class is-valid and remove class is-invalid
createJobPathInput.classList.remove('is-invalid');
createJobPathInput.classList.add('is-valid');
createJobPathFeedback.style.display = 'none';
addDestinationBtn.removeAttribute('disabled');
} else {
// If it doesn't exist, add class is-invalid and remove class is-valid
createJobPathInput.classList.remove('is-valid');
createJobPathInput.classList.add('is-invalid');
createJobPathFeedback.style.display = 'block';
addDestinationBtn.setAttribute('disabled', true);
}
})
.catch(error => {
console.error('Error:', error);
});
}
});
</script>
<form method="post">
<!-- Page header -->
<div class="page-header mt-0 d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<!-- Page pre-title -->
<div class="page-pretitle">
Settings
</div>
<h2 class="page-title">
Backups
</h2>
</div>
<!-- Page title actions -->
<div class="col-auto ms-auto d-print-none">
<div class="btn-list" id="notifications">
</div>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<div class="col-12 col-md-2 border-end">
<div class="card-body">
<h4 class="subheader">Configuration</h4>
<div class="list-group list-group-transparent">
<a href="#jobs" class="list-group-item list-group-item-action d-flex align-items-center active" data-bs-toggle="tab" aria-selected="true" role="tab"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-server"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" /><path d="M3 12m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" /><path d="M7 8l0 .01" /><path d="M7 16l0 .01" /></svg><span class="m-2">Backup Jobs</span></a>
<a href="#destinations" class="list-group-item list-group-item-action d-flex align-items-center" data-bs-toggle="tab" aria-selected="false" role="tab" tabindex="-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-cloud-computing"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6.657 16c-2.572 0 -4.657 -2.007 -4.657 -4.483c0 -2.475 2.085 -4.482 4.657 -4.482c.393 -1.762 1.794 -3.2 3.675 -3.773c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99c1.913 0 3.464 1.56 3.464 3.486c0 1.927 -1.551 3.487 -3.465 3.487h-11.878" /><path d="M12 16v5" /><path d="M16 16v4a1 1 0 0 0 1 1h4" /><path d="M8 16v4a1 1 0 0 1 -1 1h-4" /></svg><span class="m-2">Destinations</span></a>
<a href="#restore" class="list-group-item list-group-item-action d-flex align-items-center" data-bs-toggle="tab" aria-selected="false" role="tab" tabindex="-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-download"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><path d="M7 11l5 5l5 -5" /><path d="M12 4l0 12" /></svg><span class="m-2">Restore & Download</span></a>
<a href="#logs" class="list-group-item list-group-item-action d-flex align-items-center" data-bs-toggle="tab" aria-selected="false" role="tab" tabindex="-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-logs"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12h.01" /><path d="M4 6h.01" /><path d="M4 18h.01" /><path d="M8 18h2" /><path d="M8 12h2" /><path d="M8 6h2" /><path d="M14 6h6" /><path d="M14 12h6" /><path d="M14 18h6" /></svg><span class="m-2">Logs</span></a>
<a href="#settings" id="settings-link" class="list-group-item list-group-item-action d-flex align-items-center" data-bs-toggle="tab" aria-selected="false" role="tab" tabindex="-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-tool"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5" /></svg><span class="m-2">Settings</span></a>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('settings-link').addEventListener('click', function(event) {
event.preventDefault(); // Prevent the default link behavior
fetch('/backups/settings', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
// Update form fields with the data
var general = data.GENERAL || {};
var notifications = data.NOTIFICATIONS || {};
var performance = data.PERFORMANCE || {};
var encryptionKey = data.encryption_key || '';
// Set GENERAL settings
document.getElementById('workplace_directory').value = general.workplace_dir || '/tmp';
document.getElementById('downloads_directory').value = general.downloads_dir || '/tmp';
document.getElementById('enable_debug').checked = general.debug === 'yes';
document.getElementById('enable_debug-feedback').textContent = general.debug === 'yes' ? 'Debugging enabled' : 'Debugging disabled';
document.getElementById('delete_orphan_backups').value = general.delete_orphan_backups || '';
document.getElementById('days_to_keep_logs').value = general.days_to_keep_logs || '0';
var timeFormatSelect = document.getElementById('time_format');
var timeFormatValue = general.time_format || '12';
for (var i = 0; i < timeFormatSelect.options.length; i++) {
if (timeFormatSelect.options[i].value === timeFormatValue) {
timeFormatSelect.options[i].selected = true;
break;
}
}
// Set PERFORMANCE settings
document.getElementById('load_limit').value = performance.avg_load_limit || '0';
document.getElementById('concurrent_tasks').value = performance.concurent_jobs || '0';
document.getElementById('backup_restore_ttl').value = performance.backup_restore_ttl || '0';
document.getElementById('cpu_limit').value = performance.cpu_limit || '0';
document.getElementById('io_read_limit').value = performance.io_read_limit || '0';
document.getElementById('io_write_limit').value = performance.io_write_limit || '0';
// Set ENCRYPTION_KEY
document.getElementById('encryption_key').value = encryptionKey;
// Set NOTIFICATIONS settings
document.getElementById('send_emails_to').value = notifications.send_emails_to || '';
document.getElementById('notify_on_backup_run').checked = notifications.notify_on_every_job === 'yes';
document.getElementById('notify_on_failed_backup').checked = notifications.notify_on_failed_backups === 'yes';
document.getElementById('notify_if_no_backups_after').value = notifications.notify_if_no_backups_after || '0';
})
.catch(error => {
console.error('Error fetching settings:', error);
});
});
});
</script>
</div>
</div>
<div class="col-12 col-md-10 d-flex flex-column">
<div class="tab-content">
<div class="tab-pane card-body active show" id="jobs" role="tabpanel" style="overflow: scroll;">
<div class="row g-2 align-items-center">
<div class="col">
<h4 class="page-title">
Backup Jobs
</h4>
<p class="page-subtitle">
Backup Jobs allows you to configure how OpenPanel generates and manages your backups according to your backup job configuration.
</p>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<a href="#" data-bs-toggle="modal" data-bs-target="#createJobModal" class="btn btn-primary d-none d-sm-inline-block" id="backupButton"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 5l0 14"></path><path d="M5 12l14 0"></path></svg>New Backup Job</a>
<script>
// Add an event listener to the button for click event
document.getElementById('backupButton').addEventListener('click', function() {
// Send AJAX request to /backups/destination
$.ajax({
url: '/backups/destination',
method: 'GET',
success: function(data) {
// Update the select dropdown with JSON data
updateSelectDropdown(data);
},
error: function(err) {
console.error('Error fetching data:', err);
}
});
});
// Function to update the select dropdown with JSON data
function updateSelectDropdown(data) {
var selectDropdown = document.getElementById('backupSelect');
// Clear existing options
selectDropdown.innerHTML = '';
// Loop through the JSON data and create options
data.forEach(function(item) {
var option = document.createElement('option');
// Set the value attribute to the ID
option.value = item.id;
// Set the inner text to the hostname
option.innerText = item.configuration.hostname;
// Append the option to the select dropdown
selectDropdown.appendChild(option);
});
}
</script>
</div>
</div>
</div>
{% if error %}
<p>{{ error }}</p>
{% else %}
<table class="table card-table table-vcenter text-nowrap datatable">
<thead>
<tr>
<th>Name</th>
<th>Destination</th>
<th>Type</th>
<th>Schedule</th>
<th>Status</th>
<th>Filters</th>
<th></th>
</tr>
</thead>
<tbody>
{% for job in backup_jobs %}
<tr>
<td>{{ job.name }}</td>
<td>{% if "wasabi" in job.destination %}<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-bucket" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 7m-8 0a8 4 0 1 0 16 0a8 4 0 1 0 -16 0" /><path d="M4 7c0 .664 .088 1.324 .263 1.965l2.737 10.035c.5 1.5 2.239 2 5 2s4.5 -.5 5 -2c.333 -1 1.246 -4.345 2.737 -10.035a7.45 7.45 0 0 0 .263 -1.965" /></svg> {% elif "local" in job.destination %}<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2" /></svg> {% else %}<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2" /></svg> {% endif %} {{ job.destination }}</td>
<td>
<div style="display: grid;">
{% for type in job.type %}
{% if "accounts" in type %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-users"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /><path d="M21 21v-2a4 4 0 0 0 -3 -3.85" /></svg>
{% elif "partial" in type %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-users-plus"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M3 21v-2a4 4 0 0 1 4 -4h4c.96 0 1.84 .338 2.53 .901" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /><path d="M16 19h6" /><path d="M19 16v6" /></svg>
{% elif "config" in type %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-folder-cog"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12.5 19h-7.5a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v3" /><path d="M19.001 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M19.001 15.5v1.5" /><path d="M19.001 21v1.5" /><path d="M22.032 17.25l-1.299 .75" /><path d="M17.27 20l-1.3 .75" /><path d="M15.97 17.25l1.3 .75" /><path d="M20.733 20l1.3 .75" /></svg>
{% endif %}
<small>{{ type.capitalize() }}</small>
{% endfor %}
</div>
</td>
<td class="text-nowrap">
<small>
{% if "hour" in job.schedule %}<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calendar-clock" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.5 21h-4.5a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v3" /><path d="M16 3v4" /><path d="M8 3v4" /><path d="M4 11h10" /><path d="M18 18m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M18 16.5v1.5l.5 .5" /></svg>
{% elif "daily" in job.schedule %}<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calendar" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z" /><path d="M16 3v4" /><path d="M8 3v4" /><path d="M4 11h16" /><path d="M11 15h1" /><path d="M12 15v3" /></svg>
{% elif "week" in job.schedule %}<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calendar-week" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z" /><path d="M16 3v4" /><path d="M8 3v4" /><path d="M4 11h16" /><path d="M8 14v4" /><path d="M12 14v4" /><path d="M16 14v4" /></svg>
{% elif "month" in job.schedule %}<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calendar-month" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z" /><path d="M16 3v4" /><path d="M8 3v4" /><path d="M4 11h16" /><path d="M7 14h.013" /><path d="M10.01 14h.005" /><path d="M13.01 14h.005" /><path d="M16.015 14h.005" /><path d="M13.015 17h.005" /><path d="M7.01 17h.005" /><path d="M10.01 17h.005" /></svg>
{% else %}<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calendar-clock" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.5 21h-4.5a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v3" /><path d="M16 3v4" /><path d="M8 3v4" /><path d="M4 11h10" /><path d="M18 18m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M18 16.5v1.5l.5 .5" /></svg>
{% endif%} {{ job.schedule }}<br><span class="text-secondary">Retain {{ job.retention }} Backups</b></small>
</td>
<td><span class="badge bg-{% if "on" in job.status %}success{% elif "off" in job.status %}danger{% endif%} me-1"></span>{{ job.status.capitalize() }}</td>
<td>
<div class="badges-list">
{% for filter_value in job.filters %}
{% if filter_value == 'mysql' %}
<span class="badge bg-blue-lt mb-1">MySQL</span>
{% elif filter_value == 'php' %}
<span class="badge bg-blue-lt mb-1">PHP</span>
{% elif filter_value == 'ssh' %}
<span class="badge bg-blue-lt mb-1">SSH</span>
{% else %}
<span class="badge bg-blue-lt mb-1">{{ filter_value|capitalize }}</span>
{% endif %}
{% endfor %}
</div>
</td>
<td class="text-end">
<span class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-boundary="viewport" data-bs-toggle="dropdown">Actions</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item btn btn-primary" style="display: block;" href="#{{ job.id }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-edit"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" /><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" /><path d="M16 5l3 3" /></svg> Manage Job
</a>
<a class="dropdown-item run-job btn btn-success" style="display: block;" data-job-id="{{ job.id }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-player-play"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 4v16l13 -8z" /></svg> Run Now
</a>
<a class="dropdown-item d-none btn btn-secondary" style="display: block;" href="/backups/export/{{ job.id }}" data-logs-id="{{ job.id }}">Export</a>
<a class="dropdown-item d-none btn btn-secondary" style="display: block;" href="#logs-id" data-logs-id="{{ job.id }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-align-box-left-bottom"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 3m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M9 17h-2" /><path d="M13 14h-6" /><path d="M11 11h-4" /></svg> View Logs
</a>
<a class="dropdown-item delete-job-button btn btn-danger" data-bs-toggle="modal" data-bs-target="#confirmDeleteJobModal" style="display: block;" data-deletejob-id="{{ job.id }}" data-deletejob-name="{{ job.name }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-trash"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" /></svg> Delete
</a>
</div>
</span>
</td>
</tr>
{% endfor %}
</tbody></table>
<script>
$(document).ready(function(){
// Function to handle the click event on "Run Now" action
$(".run-job").click(function(){
// Get the job ID from the ID attribute of the clicked element
var jobId = $(this).attr("data-job-id");
// Send a POST request to the Flask endpoint
$.ajax({
url: '/backups/jobs/run/' + jobId,
type: 'POST',
success: function(response){
if (response.message === 'Backup started successfully.') {
// Show success message
var successStartedJob = $('<div class="alert alert-success alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div><div>Backup job started successfully!</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
successStartedJob.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
successStartedJob.fadeOut(500, function() {
successStartedJob.remove();
});
}, 5000);
} else if (response.status === 'failure') {
// Show failure message
var errorStartedJob = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><!-- Download SVG icon from http://tabler-icons.io/i/alert-circle --><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>' + response.message + '</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
errorStartedJob.appendTo('#notifications');
// Hide the error message after 5 seconds
setTimeout(function() {
errorStartedJob.fadeOut(500, function() {
errorStartedJob.remove();
});
}, 5000);
}
},
error: function(xhr, status, error){
// Show an error message if the request fails
alert("Error: " + error);
// Show generic error message
var errorStartingJob = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><!-- Download SVG icon from http://tabler-icons.io/i/alert-circle --><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>' + error + '</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
errorStartingJob.appendTo('#notifications');
// Hide the error message after 5 seconds
setTimeout(function() {
errorStartingJob.fadeOut(500, function() {
errorStartingJob.remove();
});
}, 5000);
}
});
});
});
</script>
{% endif %}
</div>
<div class="tab-pane card-body" id="destinations" role="tabpanel" style="overflow: scroll;">
<div class="row g-2 align-items-center">
<div class="col">
<h4 class="page-title">
Destinations
</h4>
<p class="page-subtitle">
A Backup destination serves as the storage location for your backup files (backup destinations can be remote or local)
</p>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<a href="#" data-bs-toggle="modal" data-bs-target="#createModal" class="btn btn-primary d-none d-sm-inline-block" >
<!-- Download SVG icon from http://tabler-icons.io/i/plus -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 5l0 14"></path><path d="M5 12l14 0"></path></svg>New Destination</a>
</div>
</div>
</div>
<script>
// JOB
var deleteJobButtons = document.getElementsByClassName("delete-job-button");
// Check if there are any elements with the class
if (deleteJobButtons.length > 0) {
// Loop through the collection and add the event listener to each element
for (var i = 0; i < deleteJobButtons.length; i++) {
deleteJobButtons[i].addEventListener("click", function(event) {
var jobId = this.getAttribute('data-deletejob-id');
var jobName = this.getAttribute('data-deletejob-name');
var jobToDeleteElement = document.getElementById("job_to_delete");
if (jobToDeleteElement) {
jobToDeleteElement.textContent = jobId; // Set the text content of the element
}
var jobToDeleteName = document.getElementById("job_to_delete_name");
if (jobToDeleteName) {
jobToDeleteName.textContent = jobName; // Set the text content of the element
}
// Clear the input field and hide the Delete button when the modal is shown
var deleteJobConfirmation = document.getElementById("deleteJobConfirmation");
var confirmJobDeleteButton = document.getElementById("confirmJobDelete");
if (deleteJobConfirmation && confirmJobDeleteButton) {
deleteJobConfirmation.value = "";
confirmJobDeleteButton.style.display = "none";
}
});
}
}
var deleteJobConfirmation = document.getElementById("deleteJobConfirmation");
if (deleteJobConfirmation) {
deleteJobConfirmation.addEventListener("input", function() {
// Toggle the Terminate button visibility based on the input value
var confirmationInput = this.value.trim().toUpperCase();
var confirmJobDeleteButton = document.getElementById("confirmJobDelete");
if (confirmJobDeleteButton) {
confirmJobDeleteButton.style.display = (confirmationInput === "DELETE") ? "block" : "none";
}
});
}
var confirmJobDeleteButton = document.getElementById("confirmJobDelete");
if (confirmJobDeleteButton) {
confirmJobDeleteButton.addEventListener("click", function() {
// Proceed with the termination
var jobToDeleteElement = document.getElementById("job_to_delete");
var jobId = jobToDeleteElement ? jobToDeleteElement.textContent : null;
console.log("Job ID to delete:", jobId);
var deleteModalxClose = document.getElementById("deleteJobModalxClose");
var confirmJobDeleteModal = document.getElementById("confirmJobDeleteUserModal");
var modalFooter = confirmJobDeleteModal ? confirmJobDeleteModal.getElementsByClassName("modal-footer")[0] : null;
if (confirmJobDeleteModal) {
confirmJobDeleteModal.setAttribute("data-bs-backdrop", "static");
confirmJobDeleteModal.setAttribute("data-bs-keyboard", "false");
}
if (modalFooter) {
modalFooter.style.display = "none";
}
if (deleteModalxClose) {
deleteModalxClose.style.display = "none";
}
// Update modal content to indicate deletion is in progress
var modalBody = confirmJobDeleteModal ? confirmJobDeleteModal.getElementsByClassName("modal-body")[0] : null;
if (modalBody) {
modalBody.innerHTML = `
<div class="text-center">
<div class="text-secondary mb-3">Deleting backup job, please wait..</div>
<div class="progress progress-sm">
<div class="progress-bar progress-bar-indeterminate"></div>
</div>
</div>
`;
}
fetch(`/backups/jobs`, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ id: jobId })
})
.then(response => response.json())
.then(data => {
if (data.message.includes("Backup job deleted successfully.")) {
window.location.reload();
} else {
console.error("Error deleting backup job:", data.message);
}
})
.catch(error => {
console.error("Error sending request:", error);
});
// Hide the modal
var modal = new bootstrap.Modal(confirmJobDeleteModal);
modal.hide();
});
}
</script>
<script>
// DESTINATION
var deleteDestinationButtons = document.getElementsByClassName("delete-destination-button");
// Check if there are any elements with the class
if (deleteDestinationButtons.length > 0) {
// Loop through the collection and add the event listener to each element
for (var i = 0; i < deleteDestinationButtons.length; i++) {
deleteDestinationButtons[i].addEventListener("click", function(event) {
var destinationId = this.getAttribute('data-deletedestination-id');
var destinationToDeleteElement = document.getElementById("destination_to_delete");
if (destinationToDeleteElement) {
destinationToDeleteElement.textContent = destinationId; // Set the text content of the element
}
var destinationName = this.getAttribute('data-deletedestination-name');
var destinationToDeleteName = document.getElementById("destination_to_delete_name");
if (destinationToDeleteName) {
destinationToDeleteName.textContent = destinationName; // Set the text content of the element
}
event.preventDefault();
// Clear the input field and hide the Delete button when the modal is shown
var deleteConfirmation = document.getElementById("deleteConfirmation");
var confirmDelete = document.getElementById("confirmDelete");
if (deleteConfirmation && confirmDelete) {
deleteConfirmation.value = "";
confirmDelete.style.display = "none";
}
});
}
}
var deleteConfirmation = document.getElementById("deleteConfirmation");
if (deleteConfirmation) {
deleteConfirmation.addEventListener("input", function() {
// Toggle the Terminate button visibility based on the input value
var confirmationInput = this.value.trim().toUpperCase();
var confirmDelete = document.getElementById("confirmDelete");
if (confirmDelete) {
confirmDelete.style.display = (confirmationInput === "DELETE") ? "block" : "none";
}
});
}
var confirmDelete = document.getElementById("confirmDelete");
if (confirmDelete) {
confirmDelete.addEventListener("click", function() {
// Proceed with the termination
var destinationToDeleteElement = document.getElementById("destination_to_delete");
var destinationId = destinationToDeleteElement ? destinationToDeleteElement.textContent : null;
console.log("Destination ID to delete:", destinationId);
var deleteModalxClose = document.getElementById("deleteModalxClose");
var confirmDeleteDestinationModal = document.getElementById("confirmDeleteDestinationModal");
var modalFooter = confirmDeleteDestinationModal ? confirmDeleteDestinationModal.querySelector(".modal-footer") : null;
if (confirmDeleteDestinationModal) {
confirmDeleteDestinationModal.setAttribute("data-bs-backdrop", "static");
confirmDeleteDestinationModal.setAttribute("data-bs-keyboard", "false");
}
if (modalFooter) {
modalFooter.style.display = "none";
}
if (deleteModalxClose) {
deleteModalxClose.style.display = "none";
}
// Update modal content to indicate deletion is in progress
var modalBody = confirmDeleteDestinationModal ? confirmDeleteDestinationModal.querySelector(".modal-body") : null;
if (modalBody) {
modalBody.innerHTML = `
<div class="text-center">
<div class="text-secondary mb-3">Deleting destination, please wait..</div>
<div class="progress progress-sm">
<div class="progress-bar progress-bar-indeterminate"></div>
</div>
</div>
`;
}
fetch(`/backups/destination/delete/${destinationId}`, {
method: "POST",
})
.then(response => response.json())
.then(data => {
if (data.message.includes("Deleted destination ID")) {
window.location.reload();
} else {
console.error("Error deleting destination:", data.message);
}
})
.catch(error => {
console.error("Error sending request:", error);
});
// Hide the modal
if (confirmDeleteDestinationModal) {
var modal = new bootstrap.Modal(confirmDeleteDestinationModal);
modal.hide();
}
});
}
</script>
<!-- Table to display destination data -->
<table class="table table-hover">
<thead>
<tr>
<th>Destination</th>
<th>Type</th>
<th>SSH User</th>
<th>SSH Port</th>
<th>SSH Key</th>
<th>Storage Limit</th>
<th></th>
</tr>
</thead>
<tbody id="destinations-table-body">
</tbody>
</table>
<!-- create destination modal -->
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createModalLabel">Add a New Destination</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Input fields for editing destination data -->
<form id="createForm">
<label class="form-label">Destination type</label>
<div class="form-selectgroup-boxes row mb-3">
<div class="col-lg-6">
<label class="form-selectgroup-item">
<input type="radio" name="destination-type" value="1" class="form-selectgroup-input" checked="">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Remote</span>
<span class="d-block text-secondary">Backup to remote server using SSH and key based authentication</span>
</span>
</span>
</label>
</div>
<div class="col-lg-6">
<label class="form-selectgroup-item">
<input type="radio" name="destination-type" value="1" class="form-selectgroup-input">
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Local</span>
<span class="d-block text-secondary">Backup to this server in specified directory (not recommended)</span>
</span>
</span>
</label>
</div>
</div>
<div class="row">
<div id="hostname_value_column" class="col-lg-9">
<div class="mb-3">
<label for="create-hostname" class="form-label">Domain or IP address</label>
<input type="text" class="form-control" id="create-hostname">
</div>
</div>
<div class="col-lg-3">
<div class="mb-3" id="create-ssh-port-column">
<label for="create-ssh-port" class="form-label">SSH Port</label>
<input type="number" class="form-control" id="create-ssh-port" min="22" max="35000">
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="mb-3" id="create-ssh-user-column">
<label for="create-ssh-user" class="form-label">SSH User</label>
<input type="text" class="form-control" id="create-ssh-user">
</div>
</div>
<div class="col-lg-6">
<div class="mb-3" id="create-ssh-key-path-column">
<label for="create-ssh-key-path" class="form-label">SSH Key Path</label>
<input type="text" class="form-control" id="create-ssh-key-path">
<div id="ssh-key-path-feedback" class="invalid-feedback">SSH Key file does not exist.</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// ... (Your existing code for other fields)
var sshKeyPathField = document.getElementById('create-ssh-key-path');
var sshKeyPathFeedback = document.getElementById('ssh-key-path-feedback');
var addDestinationBtn = document.getElementById('addNewDestinationBtn');
sshKeyPathField.addEventListener('input', function () {
// Get the input value
var inputValue = sshKeyPathField.value;
// Make an asynchronous request to the Flask route for file existence check
fetch('/backups/helpers/path?file=' + inputValue)
.then(response => response.json())
.then(data => {
// Check if the file exists
if (data.exists) {
// If it exists, remove 'is-invalid' class and hide feedback
sshKeyPathField.classList.remove('is-invalid');
sshKeyPathField.classList.add('is-valid');
sshKeyPathFeedback.style.display = 'none';
addDestinationBtn.removeAttribute('disabled');
} else {
// If it doesn't exist, add 'is-invalid' class and show feedback
sshKeyPathField.classList.remove('is-valid');
sshKeyPathField.classList.add('is-invalid');
sshKeyPathFeedback.style.display = 'block';
addDestinationBtn.setAttribute('disabled', true);
}
})
.catch(error => {
console.error('Error:', error);
});
});
});
</script>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="mb-3">
<label for="create-storage-limit" class="form-label">Storage Limit</label>
<div class="input-group mb-0">
<input type="number" autocomplete="off" class="form-control" id="create-storage-limit" min="1" max="100">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function () {
var destinationTypeRadio = document.getElementsByName('destination-type');
var hostnameField = document.getElementById('create-hostname');
var hostnameValueColumn = document.getElementById('hostname_value_column');
var sshPortField = document.getElementById('create-ssh-port');
var sshUserField = document.getElementById('create-ssh-user');
var sshKeyPathField = document.getElementById('create-ssh-key-path');
var sshPortFieldColumn = document.getElementById('create-ssh-port-column');
var sshUserFieldColumn = document.getElementById('create-ssh-user-column');
var sshKeyPathFieldColumn = document.getElementById('create-ssh-key-path-column');
function toggleFields() {
if (destinationTypeRadio[1].checked) { // 'Local' is at index 1
hostnameField.value = 'localhost';
hostnameField.disabled = true;
// Set values and hide SSH-related fields
sshPortField.value = '11';
sshUserField.value = 'localhost';
sshKeyPathField.value = 'localhost';
sshPortFieldColumn.style.display = 'none';
sshUserFieldColumn.style.display = 'none';
sshKeyPathFieldColumn.style.display = 'none';
// Change class for 'Local'
hostnameValueColumn.className = 'col-lg-12';
} else {
hostnameField.value = ''; // Empty the field for 'Remote'
hostnameField.disabled = false;
// Clear values and show SSH-related fields
sshPortField.value = '';
sshUserField.value = '';
sshKeyPathField.value = '';
sshPortFieldColumn.style.display = 'block';
sshUserFieldColumn.style.display = 'block';
sshKeyPathFieldColumn.style.display = 'block';
// Change class for 'Remote'
hostnameValueColumn.className = 'col-lg-9';
}
}
// Initial state
toggleFields();
// Add event listener to each radio button
destinationTypeRadio.forEach(function (radio) {
radio.addEventListener('change', toggleFields);
});
});
</script>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="submitChanges()" id="addNewDestinationBtn">Add Destination</button>
</div>
</div>
</div>
</div>
<!-- edit destination modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit Destination</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Input fields for editing destination data -->
<form id="editForm">
<input type="hidden" id="edit-destination-id">
<div class="mb-3">
<label for="edit-hostname" class="form-label">Domain or IP address</label>
<input type="text" class="form-control" id="edit-hostname">
</div>
<div class="mb-3">
<label for="edit-ssh-user" class="form-label">SSH User</label>
<input type="text" class="form-control" id="edit-ssh-user">
</div>
<div class="mb-3">
<label for="edit-ssh-port" class="form-label">SSH Port</label>
<input type="number" class="form-control" id="edit-ssh-port" min="22" max="35000">
</div>
<div class="mb-3">
<label for="edit-ssh-key-path" class="form-label">SSH Key Path</label>
<input type="text" class="form-control" id="edit-ssh-key-path">
</div>
<div class="mb-3">
<label for="edit-storage-limit" class="form-label">Storage Limit</label>
<div class="input-group mb-0">
<input type="number" autocomplete="off" class="form-control" id="edit-storage-limit" min="1" max="100">
<span class="input-group-text">%</span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="saveChanges()">Save Changes</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
// Function to handle the "Delete User" button click
$('.destination_delete').click(function () {
// Retrieve data from the button's data attributes
var destination = $(this).data('destination');
// Set the values of modal elements
$('#destinationDelete').text(destination);
$('#destinationToDeletehidden').val(destination);
});
});
// Function to handle saving new destination in the modal
function submitChanges() {
// Get data from input fields
var hostname = $('#create-hostname').val();
var sshUser = $('#create-ssh-user').val();
var sshPort = $('#create-ssh-port').val();
var sshKeyPath = $('#create-ssh-key-path').val();
var storageLimit = $('#create-storage-limit').val();
// Prepare data to send to the server
var postData = {
hostname: hostname,
user: sshUser,
port: sshPort,
path_to_ssh_key_file: sshKeyPath,
storage_percentage: storageLimit
};
// Make an Ajax request to update the destination on the server
$.ajax({
url: '/backups/destination/create/007',
type: 'POST',
data: postData,
success: function(response) {
// Handle the server response (success or error)
if (response.status === 'success') {
//close modal
$('#createModal').modal('hide');
//refresh table data immediately
updateDestinationsTable();
// Show success message
var successMessage = $('<div class="alert alert-success alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div><div>New destination added successfully!</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
successMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
successMessage.fadeOut(500, function() {
successMessage.remove();
});
}, 5000);
} else if (response.status === 'failure') {
// Show failure message
showCreateErrorMessage('Error creating new destination: ' + response.result);
} else {
// Show error message
showCreateErrorMessage('Error creating new destination: ' + response.result);
}
// Close the modal after creatin
//$('#editModal').modal('hide');
},
error: function(error) {
console.error('Error creating destination:', error);
// Show error message
showCreateErrorMessage('Error saving changes. Please try again.');
}
});
}
// Function to show success message on top of the modal
function showCreateSuccessMessage(message) {
// Display the success message
var successMessage = $('<div class="alert alert-success" role="alert">' + message + '</div>');
$('#createModal .modal-body').prepend(successMessage);
// Remove the message after a few seconds (you can adjust the delay as needed)
setTimeout(function() {
successMessage.remove();
}, 5000);
}
// Function to show error message on top of the modal
function showCreateErrorMessage(message) {
// Display the error message
var errorMessage = $('<div class="alert alert-danger" role="alert">' + message + '</div>');
$('#createModal .modal-body').prepend(errorMessage);
// Remove the message after a few seconds (you can adjust the delay as needed)
setTimeout(function() {
errorMessage.remove();
}, 5000);
}
function openEditModal(destinationId) {
// Make an Ajax request to fetch destination data
$.ajax({
url: '/backups/destination/' + destinationId,
type: 'GET',
dataType: 'json',
success: function(response) {
// Populate input fields with the fetched data
$('#edit-destination-id').val(destinationId);
$('#edit-hostname').val(response.configuration.hostname);
$('#edit-ssh-user').val(response.configuration.ssh_user);
$('#edit-ssh-port').val(response.configuration.ssh_port);
$('#edit-ssh-key-path').val(response.configuration.ssh_key_path);
$('#edit-storage-limit').val(response.configuration.storage_limit);
// Show the modal
$('#editModal').modal('show');
},
error: function(error) {
console.error('Error fetching data from /backups/destination/' + destinationId + ':', error);
}
});
}
// Function to handle saving changes in the modal
function saveChanges() {
// Get data from input fields
var destinationId = $('#edit-destination-id').val();
var hostname = $('#edit-hostname').val();
var sshUser = $('#edit-ssh-user').val();
var sshPort = $('#edit-ssh-port').val();
var sshKeyPath = $('#edit-ssh-key-path').val();
var storageLimit = $('#edit-storage-limit').val();
// Prepare data to send to the server
var postData = {
id: destinationId,
hostname: hostname,
user: sshUser,
port: sshPort,
path_to_ssh_key_file: sshKeyPath,
storage_percentage: storageLimit
};
// Make an Ajax request to update the destination on the server
$.ajax({
url: '/backups/destination/edit/' + destinationId,
type: 'POST',
data: postData,
success: function(response) {
// Handle the server response (success or error)
if (response.status === 'success') {
//refresh table data immediately
updateDestinationsTable();
// Show success message
showSuccessMessage('Changes saved successfully!');
} else if (response.status === 'failure') {
// Show failure message
showErrorMessage('Error saving changes: ' + response.result);
} else {
// Show error message
showErrorMessage('Error saving changes: ' + response.result);
}
// Close the modal after saving changes
//$('#editModal').modal('hide');
},
error: function(error) {
console.error('Error updating destination:', error);
// Show error message
showErrorMessage('Error saving changes. Please try again.');
}
});
}
// Function to show success message on top of the modal
function showSuccessMessage(message) {
// Display the success message
var successMessage = $('<div class="alert alert-success" role="alert">' + message + '</div>');
$('#editModal .modal-body').prepend(successMessage);
// Remove the message after a few seconds (you can adjust the delay as needed)
setTimeout(function() {
successMessage.remove();
}, 5000);
}
// Function to show error message on top of the modal
function showErrorMessage(message) {
// Display the error message
var errorMessage = $('<div class="alert alert-danger" role="alert">' + message + '</div>');
$('#editModal .modal-body').prepend(errorMessage);
// Remove the message after a few seconds (you can adjust the delay as needed)
setTimeout(function() {
errorMessage.remove();
}, 5000);
}
// Function to make an asynchronous request to /backups/destination and update the table
function updateDestinationsTable() {
$.ajax({
url: '/backups/destination',
type: 'GET',
dataType: 'json',
success: function(data) {
// Clear existing table rows
$('#destinations-table-body').empty();
// Populate the table with data
data.forEach(function(destination) {
// Check if the destination has an "error" property
if (destination.error) {
console.error('Error in destination:', destination.error);
// Skip processing invalid destinations
return;
}
var sshInfo = '';
// Check if destination.hostname is one of the specified values
if (['localhost', '127.0.0.1', '{{ force_domain_value }}', '{{ server_hostname }}', '{{ public_ip }}'].includes(destination.configuration.hostname)) {
// Don't show SSH-related information
sshInfo = '<td colspan="3"> </td>';
destinationType = '<span class="badge bg-red text-red-fg">Local</span>';
} else {
// Show SSH-related information
sshInfo = '<td class="text-nowrap text-secondary"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /></svg> ' + destination.configuration.ssh_user + '</td>' +
'<td class="text-nowrap text-secondary">' + destination.configuration.ssh_port + '</td>' +
'<td class="text-nowrap text-secondary">' + destination.configuration.ssh_key_path + '</td>';
destinationType = '<span class="badge bg-cyan text-cyan-fg">SSH</span>';
}
$('#destinations-table-body').append(
'<tr>' +
'<td>' + destination.configuration.hostname + '</td>' +
'<td>' + destinationType + '</td>' +
sshInfo +
'<td>' + destination.configuration.storage_limit + '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-percentage" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M17 17m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M7 7m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M6 18l12 -12" /></svg></td>' +
'<td class="text-end"><span class="dropdown"><button class="btn dropdown-toggle align-text-top" data-bs-boundary="viewport" data-bs-toggle="dropdown">Actions</button><div class="dropdown-menu dropdown-menu-end"><a class="dropdown-item btn btn-primary" style="display: block;" id="edit_destination_' + destination.id + '" href="#edit_destination_' + destination.id + '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-edit"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" /><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" /><path d="M16 5l3 3" /></svg> Edit</a><a class="dropdown-item btn btn-success" style="display: block;" id="validate_destination_' + destination.id + '" href="#" onclick="validateBackupDestination(' + destination.id + ');"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-reload"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747" /><path d="M20 4v5h-5" /></svg> Validate</a><a class="dropdown-item btn btn-danger delete-destination-button" href="/backups/destinations/delete/' + destination.id + '" data-bs-toggle="modal" data-bs-target="#confirmDeleteDestinationModal" style="display: block;" data-deletedestination-id="' + destination.id + '" data-deletedestination-name="' + destination.configuration.hostname + '"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-trash"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" /></svg> Delete</a></div></span></td>' +
'</tr>'
);
// Update the "Edit" dropdown item with the modal opening function
$('#edit_destination_' + destination.id).on('click', function() {
openEditModal(destination.id);
});
});
},
error: function(error) {
console.error('Error fetching data from /backups/destination:', error);
}
});
}
// Call the function to update the table when the tab is clicked
$('a[data-bs-toggle="tab"][href="#destinations"]').on('shown.bs.tab', function (e) {
updateDestinationsTable();
});
function validateBackupDestination(destinationId) {
$.ajax({
type: "POST",
url: "/backups/destination/validate/" + destinationId,
success: function(response) {
// Check if the response has a success status
if (response.status === "success") {
var successMessage = $('<div class="alert alert-success alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div><div>' + response.result + '</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
successMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
successMessage.fadeOut(500, function() {
successMessage.remove();
});
}, 5000);
} else {
var errorMessage = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><!-- Download SVG icon from http://tabler-icons.io/i/alert-circle --><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>' + response.result + '</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
errorMessage.appendTo('#notifications');
// Hide the error message after 5 seconds
setTimeout(function() {
errorMessage.fadeOut(500, function() {
errorMessage.remove();
});
}, 5000);
}
},
error: function(error) {
// Handle AJAX error
console.error("AJAX error:", error);
}
});
}
</script>
</div>
<div class="tab-pane card-body" id="restore" role="tabpanel" style="overflow: scroll;">
<div class="row g-2 align-items-center">
<div class="col">
<h4 class="page-title">
Restore & Download
</h4>
<p class="page-subtitle">
In this section, you can view, restore or download Account Backups and Configuration Backups generated from your Backup Jobs.
</p>
</div>
<div class="col-auto">
<span class="d-none d-sm-inline"><div class="input-icon"><span class="input-icon-addon"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"></path><path d="M21 21l-6 -6"></path></svg></span><input type="text" id="userSearchInput" class="form-control" placeholder="Search users…" aria-label="Search"></div></span>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
</div>
</div>
</div>
<div id="restore-container"></div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<script>
$(document).ready(function() {
function handleRestoreTabClick() {
$.ajax({
url: '/backups/restore',
type: 'GET',
dataType: 'json',
success: function(data) {
var restoreContainer = $('#restore-container');
restoreContainer.empty(); // Clear previous content
// Create a single table
var table = $('<table id="restore_table" class="table"></table>');
table.append('<thead><tr><th>Username</th><th>Backup Count</th></tr></thead>');
var tbody = $('<tbody></tbody>');
// Iterate through the response and create table rows
Object.keys(data).forEach(function(username) {
var row = $('<tr class="username-row" style="cursor: pointer;" data-username="' + username + '"></tr>');
row.append('<td>' + username + '</td>'); // Username
row.append('<td>' + data[username] + '</td>'); // Backup Count
tbody.append(row);
});
table.append(tbody);
restoreContainer.append(table);
// FILTER USERS TABLE IN RESTORE TAB
function updateTableRows(searchTerm) {
const rows = document.querySelectorAll('#restore_table tbody tr');
rows.forEach(row => {
const username = row.getAttribute('data-username').toLowerCase();
if (username.includes(searchTerm.toLowerCase())) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
const searchInput = document.getElementById('userSearchInput');
if (searchInput) {
searchInput.addEventListener('input', function() {
const searchTerm = this.value.trim();
updateTableRows(searchTerm);
});
}
},
error: function(error) {
console.error('Error fetching data:', error);
}
});
}
// Function to load backup dates and display them in a new row
function loadBackupDates(username, rowElement) {
// Check if this user already has backup details opened
var detailsRow = rowElement.next('.backup-details-row');
if (detailsRow.length > 0) {
// If the details row exists, remove it (close the opened details)
detailsRow.remove();
return;
}
// Otherwise, close any previously opened rows and deselect all checkboxes
$('.backup-details-row').remove(); // Remove previous opened rows
$('.content-icon').prop('checked', false); // Deselect all checkboxes
// Otherwise, close any previously opened rows and proceed to load new data
$('.backup-details-row').remove();
function formatDate(date) {
return date.toLocaleString();
}
function formatTimestamp(timestamp) {
var year = timestamp.substring(0, 4);
var month = timestamp.substring(4, 6);
var day = timestamp.substring(6, 8);
var hours = timestamp.substring(8, 10);
var minutes = timestamp.substring(10, 12);
var date = new Date(year, month - 1, day, hours, minutes);
return formatDate(date);
}
function getContentIcons(backup) {
const iconMap = {
'FILES': 'bi-file-earmark',
'ENTRYPOINT': 'bi-play',
'WEBSERVER_CONF': 'bi-server',
'MYSQL_CONF': 'bi-database',
'TIMEZONE': 'bi-clock',
'PHP_VERSIONS': 'bi-code-slash',
'CRONTAB': 'bi-calendar',
'MYSQL_DATA': 'bi-database',
'USER_DATA': 'bi-people',
'CORE_USERS': 'bi-shield-lock',
'STATS_USERS': 'bi-graph-up',
'APACHE_SSL_CONF': 'bi-lock',
'DOMAIN_ACCESS_REPORTS': 'bi-file-earmark-text',
'SSH_PASS': 'bi-key',
'IMAGE': 'bi-ubuntu'
};
var iconsHtml = '';
const containsArray = backup.content.contains.split(',');
containsArray.forEach(item => {
if (iconMap[item]) {
const icon = `
<label class="avatar avatar-sm me-1">
<input type="checkbox" class="content-icon" value="${item}" style="display: none;" />
<i class="bi ${iconMap[item]} icon-selectable" style="padding: 0.1rem;" title="${item}" data-bs-toggle="tooltip"></i>
</label>`;
iconsHtml += icon;
}
});
if (iconsHtml.length === 0) {
iconsHtml = '-';
}
return iconsHtml;
}
// Add event listener for icon selection
$(document).on('click', '.icon-selectable', function() {
// Toggle the background color
$(this).toggleClass('text-primary');
});
// Update the download button click event
$(document).on('click', '.start_download', function(event) {
event.preventDefault(); // Prevent default action
// Find the row containing the button
var row = $(this).closest('tr');
var selectedIcons = [];
// Collect selected icons from this row
row.find('.content-icon:checked').each(function() {
selectedIcons.push($(this).val());
});
// Get data from the href of the clicked button
const href = $(this).attr('href');
const parts = href.split('/'); // url from a href
const username = parts[3]; // username is at index 3
const backupJobId = parts[4]; // backup job ID is at index 4
const backupDate = parts[5]; // backup date is at index 5
// initial download btn contetn
const originalButtonHtml = $(this).html();
// Proceed with download, possibly sending the selected icons
if (selectedIcons.length > 0) {
// Show a loading indicator (optional)
$(this).prop('disabled', true).text('Downloading..'); // Change button text to indicate processing
// You can submit the selected icons using an AJAX call or redirect to a URL
$.ajax({
url: `/backups/download_now/${username}/${backupJobId}/${backupDate}`,
type: 'POST', // Adjust if you want a different method
data: { selectedIcons: selectedIcons }, // Send selected icons
success: function(response) {
//console.log('Download initiated successfully:', response);
var successMessage = $('<div class="alert alert-success alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div><div>Generating download started successfully!</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
successMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
successMessage.fadeOut(500, function() {
successMessage.remove();
});
}, 5000);
},
error: function(xhr) {
console.error('Error initiating download:', xhr);
var errorMessage = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>There was an error initiating the download. Please try again.</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
errorMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
errorMessage.fadeOut(500, function() {
errorMessage.remove();
});
}, 5000);
},
complete: function() {
// Re-enable the button and reset text
$(this).prop('disabled', false).html(originalButtonHtml);
}.bind(this) // Bind to use the correct context for 'this'
});
} else {
var warningMessage = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>Please select at least one content from the backup to download.</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
warningMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
warningMessage.fadeOut(500, function() {
warningMessage.remove();
});
}, 5000);
}
});
// Update the restore button
$(document).on('click', '.start_restore', function(event) {
event.preventDefault(); // Prevent default action
// Find the row containing the button
var row = $(this).closest('tr');
var selectedIcons = [];
// Collect selected icons from this row
row.find('.content-icon:checked').each(function() {
selectedIcons.push($(this).val());
});
// Get data from the href of the clicked button
const href = $(this).attr('href');
const parts = href.split('/'); // url from a href
const username = parts[3]; // username is at index 3
const backupJobId = parts[4]; // backup job ID is at index 4
const backupDate = parts[5]; // backup date is at index 5
// initial download btn contetn
const originalButtonHtml = $(this).html();
// Proceed with download, possibly sending the selected icons
if (selectedIcons.length > 0) {
// Show a loading indicator (optional)
$(this).prop('disabled', true).text('Restoring...'); // Change button text to indicate processing
// You can submit the selected icons using an AJAX call or redirect to a URL
$.ajax({
url: `/backups/restore_now/${username}/${backupJobId}/${backupDate}`,
type: 'POST', // Adjust if you want a different method
data: { selectedIcons: selectedIcons }, // Send selected icons
success: function(response) {
//console.log('Download initiated successfully:', response);
var successMessage = $('<div class="alert alert-success alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div><div>Restore process started successfully!</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
successMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
successMessage.fadeOut(500, function() {
successMessage.remove();
});
}, 5000);
},
error: function(xhr) {
console.error('Error initiating restore:', xhr);
var errorMessage = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>There was an error starting restore process. Please try again.</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
errorMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
errorMessage.fadeOut(500, function() {
errorMessage.remove();
});
}, 5000);
},
complete: function() {
// Re-enable the button and reset text
$(this).prop('disabled', false).html(originalButtonHtml);
}.bind(this) // Bind to use the correct context for 'this'
});
} else {
var warningMessage = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>Please select at least one content from the backup to restore.</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
warningMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
warningMessage.fadeOut(500, function() {
warningMessage.remove();
});
}, 5000);
}
});
function getStatusHtml(status) {
let iconClass;
let textClass;
switch (status) {
case 'Completed':
iconClass = 'bi-check-circle';
textClass = 'text-success';
break;
case 'Partial':
iconClass = 'bi-exclamation-circle';
textClass = 'text-warning';
break;
case 'In Progress':
iconClass = 'bi-spinner';
textClass = 'text-primary';
break;
default:
iconClass = 'bi-question-circle';
textClass = 'text-danger';
status = 'Unknown';
}
return `<i class="bi ${iconClass} me-2 ${textClass}" title="${status}" data-bs-toggle="tooltip"></i>`;
}
$.ajax({
url: `/backups/restore/dates/${username}`,
type: 'GET',
dataType: 'json',
success: function(data) {
// Create a new row for backup details and insert it after the clicked row
var detailsRow = $('<tr class="backup-details-row" style="background-color: none!important;"><td colspan="2" class="p-0"></td></tr>');
var detailsCell = detailsRow.find('td');
if (data.backups.length === 0) {
detailsCell.html('<p class="p-2">No backups available for this user.</p>');
} else {
// Create a table for backup details
let content = `
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Created</th>
<th>Contains</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>`;
data.backups.forEach(function(backup) {
var backupContent = backup.content;
var backup_date = formatTimestamp(backup.backup_date);
var statusHtml = getStatusHtml(backup.content.status);
// Create a row for each backup date
content += `
<tr class=":hover ">
<td class="text-nowrap">${backup_date}</td>
<td class="text-break">${getContentIcons(backup)}</td>
<td>${statusHtml}</td>
<td class="text-nowrap">
<a href="/backups/download_now/${username}/${backupContent.backup_job_id}/${backup.backup_date}" class="btn btn-secondary start_download" role="button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-download"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><path d="M7 11l5 5l5 -5" /><path d="M12 4l0 12" /></svg> Download</a>
<a href="/backups/restore_now/${username}/${backupContent.backup_job_id}/${backup.backup_date}" class="btn btn-primary start_restore" role="button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-restore"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3.06 13a9 9 0 1 0 .49 -4.087" /><path d="M3 4.001v5h5" /><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg> Restore</a>
</td>
</tr>`;
});
content += `</tbody></table>`;
detailsCell.html(content); // Insert the table into the new row
}
// Insert the new row right after the clicked username row
rowElement.after(detailsRow);
$('[data-bs-toggle="tooltip"]').tooltip(); // Initialize Bootstrap tooltips
},
error: function(xhr) {
console.error('Error fetching backup dates:', xhr);
var detailsRow = $('<tr class="backup-details-row"><td colspan="2">Error loading backup dates: ' + xhr.responseText || 'Unknown error' + '</td></tr>');
rowElement.after(detailsRow);
}
});
}
// Handle username row click to load backup dates
$(document).on('click', '.username-row', function() {
var username = $(this).data('username');
var rowElement = $(this);
// Load backup dates and display them in a row below the clicked row or close it if already open
loadBackupDates(username, rowElement);
});
// Add an input event listener to the search bar
$('#restoreSearch').on('input', function() {
var searchTerm = $(this).val().toLowerCase();
// Iterate through each row in the specific table and show/hide based on search term
$('#restore_table tbody tr.username-row').each(function() {
var rowText = $(this).text().toLowerCase();
$(this).toggle(rowText.includes(searchTerm));
});
});
$(document).on('click', 'a[href^="#restore"]', function(event) {
event.preventDefault();
handleRestoreTabClick();
});
if (window.location.href.indexOf("#restore") > -1) {
handleRestoreTabClick();
}
});
</script>
<!-- Bootstrap Modal -->
<div class="modal fade" id="backupDetailsModal" tabindex="-1" aria-labelledby="backupDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="backupDetailsModalLabel">Backup Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- dynamically -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane card-body" id="logs" role="tabpanel" style="overflow: scroll;">
<div class="row g-2 align-items-center">
<div class="col">
<h4 class="page-title">
Logs
</h4>
<p class="page-subtitle">
View and download all logs for Backup-related processes and check their corresponding status.
</p>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list"><input type="text" id="logSearch" class="form-control" placeholder="Search logs..."></div>
</div>
</div>
<div id="logs-container">
</div>
<!-- Bootstrap modal for log file content -->
<div class="modal fade" id="logModal" tabindex="-1" aria-labelledby="logModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="logModalLabel">Log File Content</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body card-body-scrollable card-body-scrollable-shadow" style="height: 28rem">
<p class="mb-0"><strong>Type:</strong> <span id="type"></span></p>
<p class="mb-0"><strong>Process ID:</strong> <span id="process_id"></span></p>
<p class="mb-0"><strong>Start Time:</strong> <span id="start_time"></span></p>
<p class="mb-0"><strong>End Time:</strong> <span id="end_time"></span></p>
<p class="mb-0"><strong>Total Execution Time:</strong> <span id="total_exec_time"></span></p>
<p class="mb-2"><strong>Status:</strong> <span id="icon"></span> <span style="color:black;" id="status"></span></p>
<pre class="divide-y" id="logContent"></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
function openModal(url) {
// Extract path from the URL
const segments = url.split('/').filter(Boolean);
const path = segments.slice(-2).join('/');
//const path = url.substring(url.lastIndexOf('/') + 1);
// Use fetch to make an asynchronous request
fetch(url)
.then(response => response.text())
.then(data => {
// Split the data into lines
const lines = data.split('\n');
// Find the index of the line starting with "status"
const statusIndex = lines.findIndex(line => line.startsWith('status='));
// Extract information before the "status" line
const logInfo = lines.slice(0, statusIndex + 1).join('\n');
const logContent = lines.slice(statusIndex + 1).join('\n');
const status = logInfo.trim().toLowerCase();
document.getElementById('logContent').innerText = logContent;
// Extract and display individual pieces of information
displayLogInfo(logInfo);
// Set icons based on the status
setIconBasedOnStatus(status);
// Update the modal title with the path
document.getElementById('logModalLabel').innerText = 'Log File Content - /var/log/openpanel/admin/backups/' + path;
// Show the modal
var logModal = new bootstrap.Modal(document.getElementById('logModal'));
logModal.show();
})
.catch(error => console.error('Error fetching data:', error));
}
function setIconBasedOnStatus(status) {
const iconElement = document.getElementById('icon');
if (status.includes('progress')) {
iconElement.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-run" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="blue" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M13 4m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M4 17l5 1l.75 -1.5" /><path d="M15 21l0 -4l-4 -3l1 -6" /><path d="M7 12l0 -3l5 -1l3 3l3 1" /></svg>';
} else if (status.includes('completed')) {
iconElement.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="green" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>';
} else if (status.includes('failed')) {
iconElement.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="red" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 9v4" /><path d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0z" /><path d="M12 16h.01" /></svg>';
} else {
iconElement.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-unlink" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="grey" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M17 22v-2" /><path d="M9 15l6 -6" /><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /><path d="M20 17h2" /><path d="M2 7h2" /><path d="M7 2v2" /></svg>';
}
}
function displayLogInfo(logInfo) {
const lines = logInfo.split('\n');
lines.forEach(line => {
const keyValue = line.split('=');
if (keyValue.length === 2) {
const value = keyValue[1].trim();
const elementId = keyValue[0].toLowerCase();
const element = document.getElementById(elementId);
if (element) {
element.innerText = value;
} else {
console.error(`Element with ID ${elementId} not found.`);
}
}
});
}
</script>
<script>
$(document).ready(function() {
function handleLogsTabClick() {
$.ajax({
url: '/backups/logs',
type: 'GET',
dataType: 'json',
success: function(data) {
var logsContainer = $('#logs-container');
logsContainer.empty(); // Clear previous content
// Create a single table
var table = $('<table id="logs_table" class="table table-bordered table-striped"></table>');
table.append('<thead><tr><th>Job ID</th><th>Type</th><th>Start Time</th><th>End Time</th><th>Total Exec Time</th><th class="text-nowrap">Status</th><th class="text-nowrap"></th></tr></thead>');
var tbody = $('<tbody></tbody>');
$.each(data, function(index, directoryInfo) {
$.each(directoryInfo.log_files, function(i, logFile) {
var row = $('<tr></tr>');
row.append('<td>' + directoryInfo.job_id + '</td>');
row.append('<td>' + logFile.log_info.type + '</td>');
row.append('<td class="text-secondary"><small>' + logFile.log_info.start_time + '</small></td>');
row.append('<td class="text-secondary"><small>' + logFile.log_info.end_time + '</small></td>');
row.append('<td>' + logFile.log_info.total_exec_time + '</td>');
var statusPrefix = '';
var statusText = logFile.log_info.status.toLowerCase();
if (statusText.includes('complete')) {
statusPrefix = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="green" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>';
} else if (statusText.includes('progress') || statusText.includes('running')) {
statusPrefix = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-run" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="blue" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M13 4m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M4 17l5 1l.75 -1.5" /><path d="M15 21l0 -4l-4 -3l1 -6" /><path d="M7 12l0 -3l5 -1l3 3l3 1" /></svg>';
} else if (statusText.includes('failed')) {
statusPrefix = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="red" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 9v4" /><path d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0z" /><path d="M12 16h.01" /></svg>';
} else if (statusText.includes('partial')) {
statusPrefix = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-exclamation-circle" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="orange" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>';
} else {
statusPrefix = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-help-circle" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="black" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M12 16v.01" /><path d="M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483" /></svg>';
}
row.append('<td class="text-nowrap">' + statusPrefix + ' ' + logFile.log_info.status + '</td>');
row.append('<td class="text-nowrap"><a href="/backups/logs/download/' + directoryInfo.job_id + '/' + logFile.file + '" data-bs-toggle="tooltip" data-bs-placement="top" title="Download Log File"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-download" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><path d="M7 11l5 5l5 -5" /><path d="M12 4l0 12" /></svg></a> <a href="#" onclick="openModal(\'/backups/logs/' + directoryInfo.job_id + '/' + logFile.file + '\')" data-bs-toggle="tooltip" data-bs-placement="top" title="View Log File"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-zoom-code" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /><path d="M8 8l-2 2l2 2" /><path d="M12 8l2 2l-2 2" /></svg></a> <a id="delete_backup_log_id_' + directoryInfo.job_id + '/' + logFile.file + '" href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete Log File" onclick="openDeleteConfirmationModal(\'' + directoryInfo.job_id + '/' + logFile.file + '\')"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" /></svg></a></td>');
tbody.append(row);
});
});
table.append(tbody);
logsContainer.append(table);
},
error: function(error) {
console.error('Error fetching data:', error);
}
});
}
// Add an input event listener to the search bar
$('#logSearch').on('input', function() {
var searchTerm = $(this).val().toLowerCase();
// Iterate through each row in the specific table and show/hide based on search term
$('#logs_table tbody tr').each(function() {
var rowText = $(this).text().toLowerCase();
$(this).toggle(rowText.includes(searchTerm));
});
});
$(document).on('click', 'a[href^="#logs"]', function(event) {
event.preventDefault();
handleLogsTabClick();
});
if (window.location.href.indexOf("#logs") > -1) {
handleLogsTabClick();
}
});
</script>
<!-- Confirm Delete Log File Modal -->
<div class="modal fade" id="confirmDeleteLogFileModal" tabindex="-1" aria-labelledby="confirmDeleteLogFileModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmDeleteLogFileModalLabel">Are you sure?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">The selected log will be permanently deleted!</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmLogFileDeleteBtn">Delete</button>
</div>
</div>
</div>
</div>
<script>
// Function to open the confirmation modal
function openDeleteConfirmationModal(logFilePath) {
// Set the href for the delete button in the modal
document.getElementById('confirmLogFileDeleteBtn').setAttribute('data-href', '/backups/logs/download/' + logFilePath);
// Open the modal
var deleteModal = new bootstrap.Modal(document.getElementById('confirmDeleteLogFileModal'));
deleteModal.show();
}
var confirmDeleteBtn = document.getElementById('confirmLogFileDeleteBtn');
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', function () {
// Get the href from the data-href attribute
var deleteUrl = this.getAttribute('data-href');
// Get the modal element
var modalElement = document.getElementById('confirmDeleteLogFileModal');
if (modalElement) {
// Check if the modal is already initialized
var deleteModal = bootstrap.Modal.getInstance(modalElement);
if (deleteModal) {
deleteModal.hide();
} else {
// Initialize the modal if not already initialized
deleteModal = new bootstrap.Modal(modalElement);
deleteModal.hide();
}
} else {
console.error('Modal element not found');
}
$.ajax({
url: deleteUrl,
method: 'DELETE',
success: function (response) {
if (response.message === 'Backup Log file deleted successfully') {
// Show success message
var successDeletedLogFileMessage = $('<div class="alert alert-success alert-dismissible mb-0" role="alert"><div class="d-flex"><div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg></div><div>Backup Log File deleted successfully!</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
successDeletedLogFileMessage.appendTo('#notifications');
// Hide the success message after 5 seconds
setTimeout(function() {
successDeletedLogFileMessage.fadeOut(500, function() {
successDeletedLogFileMessage.remove();
});
}, 5000);
} else if (response.status === 'failure') {
// Show failure message
var errorDeletedLogFileMessage = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><!-- Download SVG icon from http://tabler-icons.io/i/alert-circle --><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>' + response.result + '</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
errorDeletedLogFileMessage.appendTo('#notifications');
// Hide the error message after 5 seconds
setTimeout(function() {
errorDeletedLogFileMessage.fadeOut(500, function() {
errorDeletedLogFileMessage.remove();
});
}, 5000);
}
},
error: function (error) {
// Show generic error message
var errorDeletedLogFileMessage = $('<div class="alert alert-danger alert-dismissible mb-0" role="alert"><div class="d-flex"><div><!-- Download SVG icon from http://tabler-icons.io/i/alert-circle --><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg></div><div>Error deleting Backup Log File</div></div><a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a></div>');
errorDeletedLogFileMessage.appendTo('#notifications');
// Hide the error message after 5 seconds
setTimeout(function() {
errorDeletedLogFileMessage.fadeOut(500, function() {
errorDeletedLogFileMessage.remove();
});
}, 5000);
}
});
});
}
</script>
</div>
<div class="tab-pane card-body" id="settings" role="tabpanel" style="overflow: scroll;">
<div class="row g-2 align-items-center">
<div class="col">
<h4 class="page-title">
Settings
</h4>
<p class="page-subtitle">
Configure and finetune your server backups settings.
</p>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<button type="button" class="btn btn-primary" id="saveSettingsBtn">Save Configuration</button>
</div>
</div>
</div>
<ul class="nav nav-tabs card-header-tabs nav-fill" data-bs-toggle="tabs" role="tablist">
<li class="nav-item" role="presentation">
<a href="#settings-general" class="nav-link active" data-bs-toggle="tab" aria-selected="true" role="tab"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-settings-cog" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12.003 21c-.732 .001 -1.465 -.438 -1.678 -1.317a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c.886 .215 1.325 .957 1.318 1.694" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /><path d="M19.001 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M19.001 15.5v1.5" /><path d="M19.001 21v1.5" /><path d="M22.032 17.25l-1.299 .75" /><path d="M17.27 20l-1.3 .75" /><path d="M15.97 17.25l1.3 .75" /><path d="M20.733 20l1.3 .75" /></svg> <span style="padding-left:0.3em;">General</span></a>
</li>
<li class="nav-item" role="presentation">
<a href="#settings-performance" class="nav-link" data-bs-toggle="tab" aria-selected="false" role="tab" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z" /><path d="M9 9h6v6h-6z" /><path d="M3 10h2" /><path d="M3 14h2" /><path d="M10 3v2" /><path d="M14 3v2" /><path d="M21 10h-2" /><path d="M21 14h-2" /><path d="M14 21v-2" /><path d="M10 21v-2" /></svg> <span style="padding-left:0.3em;">Performance</span></a>
</li>
<li class="nav-item" role="presentation">
<a href="#settings-notifications" class="nav-link" data-bs-toggle="tab" aria-selected="false" role="tab" tabindex="-1"><!-- Download SVG icon from http://tabler-icons.io/i/activity -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12h4l3 8l4 -16l3 8h4"></path></svg> <span style="padding-left:0.3em;">Notifications</span></a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active show" id="settings-general" role="tabpanel">
<div class="mt-6 mb-3">
<label class="form-check">
<input id="enable_debug" class="form-check-input" type="checkbox">
<span class="form-check-label">
Enable Debug
</span>
<span class="form-check-description">
Verbose output will be displayed for every backup job and restore process.
</span>
<div id="enable_debug-feedback" class="invalid-feedback">Warning: Enabling debugging will result in larger log files since each backup action, together with any utilized commands and status messages, is recorded separately. Disabling debugging is advised for production servers.</div>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked>
<span class="form-check-label">
Error Reporting
</span>
<span class="form-check-description">
This will silently send error reports to our support team. By setting this to "YES", you are helping us to find bugs and improve the backups system.
</span>
</label>
</div>
<div class="mb-3">
<label class="form-label">Workplace Directory</label>
<input type="text" id="workplace_directory" class="form-control" value="">
<div id="workplace_directory-feedback" class="invalid-feedback">Directory does not exist.</div>
<span class="form-check-description">
The workspace directory that the system will use to store all the temporary files.</br>
To prevent issues with the system performance, move the original folder to the new destination instead of creating new directory
</span>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var WorkplaceDirField = document.getElementById('workplace_directory');
var DownloadsDirField = document.getElementById('downloads_directory');
var enableDebugCheckbox = document.getElementById('enable_debug');
var enableDebugFeedback = document.getElementById('enable_debug-feedback');
var saveSettingsBtn = document.getElementById('saveSettingsBtn');
// Add input event listener to both WorkplaceDirField and DownloadsDirField
[WorkplaceDirField, DownloadsDirField].forEach(function (inputField) {
inputField.addEventListener('input', function () {
// Get the input value
var inputValue = inputField.value;
// Make an asynchronous request to the Flask route for file existence check
fetch('/backups/helpers/path?dir=' + inputValue)
.then(response => response.json())
.then(data => {
// Check if the file exists
if (data.exists) {
// If it exists, remove 'is-invalid' class and hide feedback
inputField.classList.remove('is-invalid');
inputField.classList.add('is-valid');
inputField.nextElementSibling.style.display = 'none';
} else {
// If it doesn't exist, add 'is-invalid' class and show feedback
inputField.classList.remove('is-valid');
inputField.classList.add('is-invalid');
inputField.nextElementSibling.style.display = 'block';
saveSettingsBtn.setAttribute('disabled', true);
}
// Check if any of the input fields is still invalid
var anyInvalid = Array.from([WorkplaceDirField, DownloadsDirField])
.some(field => field.classList.contains('is-invalid'));
// Enable or disable the saveSettingsBtn based on validation results
saveSettingsBtn.disabled = anyInvalid;
})
.catch(error => {
console.error('Error:', error);
});
});
});
// Add change event listener to enableDebugCheckbox
enableDebugCheckbox.addEventListener('change', function () {
// Show or hide the enableDebugFeedback based on checkbox state
enableDebugFeedback.style.display = enableDebugCheckbox.checked ? 'block' : 'none';
});
});
</script>
<div class="mb-3">
<label class="form-label">Downloads Directory</label>
<input type="text" class="form-control" id="downloads_directory" value="">
<div id="downloads_directory-feedback" class="invalid-feedback">Directory does not exist.</div>
<span class="form-check-description">
OpenPanel will use this directory to store all system downloads, include accounts backup downloads.
</span>
</div>
<div class="mb-3">
<label class="form-label">Delete orphan backups older than </label>
<div class="input-group mb-2">
<input id="delete_orphan_backups" type="number" class="form-control" min="0" max="3650" value="">
<span class="input-group-text">days</span>
</div>
<span class="form-check-description">
Orphan backups are account backup files that are not associated with any ACTIVE account.</br>
OpenPanel will automatically remove these backups if the date these backups were CREATED is older than the number of days specified in this field.
</span>
</div>
<div class="mb-3">
<label class="form-label">Days to keep logs history</label>
<div class="input-group mb-2">
<input id="days_to_keep_logs" type="number" class="form-control" min="0" value="">
<span class="input-group-text">days</span>
</div>
<span class="form-check-description">
The time to keep logs.
</span>
</div>
<div class="mb-3">
<label class="form-label">Time format</label>
<select id="time_format" class="form-select">
<option label="12 hours based (e.g. 11:00 PM)" value="12">12 hours based (e.g. 11:00 PM)</option>
<option label="24 hours based (e.g. 23:00)" value="24">24 hours based (e.g. 23:00)</option>
</select>
</div>
</div>
<div class="tab-pane" id="settings-performance" role="tabpanel">
<div class="mt-6 mb-3">
<label class="form-label">Load Limit</label>
<div class="input-group mb-2">
<input id="load_limit" type="number" class="form-control" min="0" value="">
</div>
<span class="form-check-description">OpenPanel checks average system load when starting a backup job. If system load is above this value then the backup job will not run.</br>Recommended value: 2 x Number of CPU cores.</span>
</div>
<div class="mb-3">
<label class="form-label">Concurrent Backup/Restore Tasks</label>
<input id="concurrent_tasks" type="number" class="form-control" min="0" value="">
<span class="form-check-description">Set the maximum number of concurrent backup/restore tasks that may run simultaneously.</br>NOTE: Higher value doesn't necessarily mean higher performance, we recommend 1 task per server CPU core, but no more than 5. You also need to consider limiting CPU usage (200% is a reasonable limit).</span>
</div>
<div class="mb-3">
<label class="form-label">Backup/Restore TTL</label>
<div class="input-group mb-2">
<input type="number" id="backup_restore_ttl" class="form-control" min="0" value="">
<span class="input-group-text">minutes</span>
</div>
<span class="form-check-description">Set the maximum time a backup/restore process can run for a user account. It will automatically abort once it reaches the set TTL.</span>
</div>
<div class="mb-3">
<label class="form-label">CPU Limit</label>
<div class="input-group mb-2">
<input type="number" id="cpu_limit" class="form-control" min="0" value="">
<span class="input-group-text">%</span>
</div>
<span class="form-check-description">limit CPU usage in percentage; 100% is one core.</br>Recommended values: 100% for a single/dual CPU server, anything above we recommend values between 200% - 400%</span>
</div>
<div class="mb-3">
<label class="form-label">IO Read Limit</label>
<div class="input-group mb-2">
<input type="number" id="io_read_limit" class="form-control" min="0" value="">
<span class="input-group-text">MB/s</span>
</div>
<span class="form-check-description">limit IO read usage in MB per second</span>
</div>
<div class="mb-3">
<label class="form-label">IO Write Limit</label>
<input type="text" id="io_write_limit" class="form-control" min="0" value="">
<span class="form-check-description">limit IO write usage in MB per second</span>
</div>
<div class="mb-3">
<label class="form-label">Encryption Key</label>
<div class="input-icon">
<input type="text" id="encryption_key" value="" class="form-control" placeholder="key" readonly="" disabled>
<span class="input-icon-addon">
<!-- Download SVG icon from http://tabler-icons.io/i/files -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 3v4a1 1 0 0 0 1 1h4"></path><path d="M18 17h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h4l5 5v7a2 2 0 0 1 -2 2z"></path><path d="M16 17v2a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h2"></path></svg>
</span>
</div>
<span class="d-none form-check-description">If encryption is enabled, this key is needed to restore backups.</span>
</div>
</div>
<div class="tab-pane" id="settings-notifications" role="tabpanel">
<div class="mt-6 mb-3">
<label class="form-check">
<input class="form-check-input" type="checkbox">
<span class="form-check-label">
Enable Notifications
</span>
<span class="form-check-description">
OpenPanel will display notification in the upper right corner of the OpenAdmin interface and in the Notifications Center.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked>
<span class="form-check-label">
Enable Email Notifications
</span>
<span class="form-check-description">
Receive email alerts every time backup job is executed.
</span>
</label>
</div>
<div class="mb-3">
<label class="form-label">Send emails to</label>
<input type="email" id="send_emails_to" class="form-control">
<span class="form-check-description">
Set the email address to send Emails from the system.
</span>
</div>
<div class="mb-3">
<label class="form-label">Notify me:</label>
<label class="form-check">
<input class="form-check-input" id="notify_on_backup_run" type="checkbox" checked>
<span class="form-check-label">
Everytime a backup runs
</span>
<span class="form-check-description">
Receive email alerts every time a backup job is executed.
</span>
</label>
<label class="form-check">
<input class="form-check-input" id="notify_on_failed_backup" type="checkbox" checked>
<span class="form-check-label">
Only on failed backups
</span>
<span class="form-check-description">
Receive email alerts only when backup job is not successful.
</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" checked>
<span class="form-check-label">
If there were no backups for
</span>
<span class="input-group">
<input type="number" id="notify_if_no_backups_after" class="form-control" min="1" value="">
<span class="input-group-text">days</span>
</span>
</label>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var WorkplaceDirField = document.getElementById('workplace_directory');
var DownloadsDirField = document.getElementById('downloads_directory');
var enableDebugCheckbox = document.getElementById('enable_debug');
var saveSettingsBtn = document.getElementById('saveSettingsBtn');
var deleteOrphanBackupsField = document.querySelector('#delete_orphan_backups');
var daysToKeepLogsField = document.querySelector('#days_to_keep_logs');
var timeFormatSelect = document.querySelector('#time_format');
var loadLimitField = document.querySelector('#load_limit');
var concurrentTasksField = document.querySelector('#concurrent_tasks');
var backupRestoreTTLField = document.querySelector('#backup_restore_ttl');
var cpuLimitField = document.querySelector('#cpu_limit');
var ioReadLimitField = document.querySelector('#io_read_limit');
var ioWriteLimitField = document.querySelector('#io_write_limit');
var encryptionKeyField = document.querySelector('#encryption_key');
var emailField = document.querySelector('#send_emails_to');
var notifyOnBackupRunCheckbox = document.querySelector('#notify_on_backup_run');
var notifyOnFailedBackupCheckbox = document.querySelector('#notify_on_failed_backup');
var notifyIfNoBackupForXDays = document.querySelector('#notify_if_no_backups_after');
// Function to format boolean as 'yes' or 'no'
function formatBoolean(value) {
return value ? 'yes' : 'no';
}
// Event listener for the save button
var saveSettingsBtn = document.getElementById('saveSettingsBtn');
saveSettingsBtn.addEventListener('click', function () {
// Prepare data to send
var formData = new FormData();
formData.append('workplace_dir', WorkplaceDirField.value.trim());
formData.append('downloads_dir', DownloadsDirField.value.trim());
formData.append('delete_orphan_backups', deleteOrphanBackupsField.value.trim());
formData.append('days_to_keep_logs', daysToKeepLogsField.value.trim());
formData.append('time_format', timeFormatSelect.value.trim());
formData.append('avg_load_limit', loadLimitField.value.trim());
formData.append('concurent_jobs', concurrentTasksField.value.trim());
formData.append('backup_restore_ttl', backupRestoreTTLField.value.trim());
formData.append('cpu_limit', cpuLimitField.value.trim());
formData.append('io_read_limit', ioReadLimitField.value.trim());
formData.append('io_write_limit', ioWriteLimitField.value.trim());
formData.append('enable_notification', formatBoolean(notifyOnBackupRunCheckbox.checked));
formData.append('email_notification', formatBoolean(notifyOnFailedBackupCheckbox.checked));
formData.append('send_emails_to', emailField.value.trim());
formData.append('notify_if_no_backups_after', notifyIfNoBackupForXDays.value.trim());
// Send AJAX request to update configuration
fetch('/backups/settings', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.message === "Backup settings saved") {
var successMessage = $(
'<div class="alert alert-success alert-dismissible mb-0" role="alert">' +
'<div class="d-flex">' +
'<div>' +
'<svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">' +
'<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>' +
'<path d="M5 12l5 5l10 -10"></path>' +
'</svg>' +
'</div>' +
'<div>Configuration saved</div>' +
'</div>' +
'<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>' +
'</div>'
);
successMessage.appendTo('#notifications');
setTimeout(function() {
successMessage.fadeOut(500, function() {
successMessage.remove();
});
}, 5000);
} else {
var errorMessage = $(
'<div class="alert alert-danger alert-dismissible mb-0" role="alert">' +
'<div class="d-flex">' +
'<div>' +
'<svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">' +
'<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>' +
'<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path>' +
'<path d="M12 8v4"></path>' +
'<path d="M12 16h.01"></path>' +
'</svg>' +
'</div>' +
'<div>Error: ' + data.message + '</div>' +
'</div>' +
'<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>' +
'</div>'
);
errorMessage.appendTo('#notifications');
setTimeout(function() {
errorMessage.fadeOut(500, function() {
errorMessage.remove();
});
}, 5000);
}
})
.catch(error => {
console.error('Error:', error);
var errorMessage = $(
'<div class="alert alert-danger alert-dismissible mb-0" role="alert">' +
'<div class="d-flex">' +
'<div>' +
'<svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">' +
'<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>' +
'<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path>' +
'<path d="M12 8v4"></path>' +
'<path d="M12 16h.01"></path>' +
'</svg>' +
'</div>' +
'<div>Error updating configuration: ' + error + '</div>' +
'</div>' +
'<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>' +
'</div>'
);
errorMessage.appendTo('#notifications');
setTimeout(function() {
errorMessage.fadeOut(500, function() {
errorMessage.remove();
});
}, 5000);
});
});
});
</script>
</div>
<div class="card-footer bg-transparent mt-auto">
<div class="btn-list justify-content-end">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
{% endblock %}