hospital-management/templates/core/restore_configuration.html
2025-08-12 13:33:25 +03:00

859 lines
34 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}Restore Configuration{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'core:configuration_backup' %}">Configuration Backup</a></li>
<li class="breadcrumb-item active">Restore Configuration</li>
</ul>
<div class="row align-items-center mb-3">
<div class="col">
<h1 class="page-header">Restore Configuration</h1>
<p class="text-muted">Restore system configuration from backup files</p>
</div>
<div class="col-auto">
<a href="{% url 'core:configuration_backup' %}" class="btn btn-outline-secondary">
<i class="fa fa-arrow-left me-2"></i>Back to Backups
</a>
</div>
</div>
<!-- Restore Warning -->
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-warning">
<div class="d-flex">
<div class="me-3">
<i class="fa fa-exclamation-triangle fa-2x"></i>
</div>
<div>
<h5 class="alert-heading">Important Warning</h5>
<p class="mb-2">Restoring a configuration backup will overwrite your current system settings. This action cannot be undone.</p>
<ul class="mb-0">
<li>All current configuration will be replaced</li>
<li>Services may need to be restarted</li>
<li>Users may experience temporary downtime</li>
<li>It's recommended to create a backup before proceeding</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Restore Options -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="card-title">
<i class="fa fa-database me-2"></i>
Restore from Existing Backup
</h4>
</div>
<div class="card-body">
<p class="text-muted">Select from previously created backups stored on the server.</p>
<form id="restoreExistingForm">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Available Backups</label>
<select id="existingBackupSelect" name="backup_id" class="form-select" required>
<option value="">Loading backups...</option>
</select>
<div class="form-text">Only completed backups are shown</div>
</div>
<div id="backupPreview" class="mb-3" style="display: none;">
<div class="card bg-light">
<div class="card-body">
<h6>Backup Preview</h6>
<div id="backupPreviewContent">
<!-- Backup details will be shown here -->
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-warning">
<i class="fa fa-upload me-2"></i>Restore Selected Backup
</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="card-title">
<i class="fa fa-file-upload me-2"></i>
Upload Backup File
</h4>
</div>
<div class="card-body">
<p class="text-muted">Upload a backup file from your computer to restore.</p>
<form id="restoreUploadForm" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Backup File</label>
<input type="file" id="backupFileInput" name="backup_file" class="form-control" accept=".gz,.zip,.tar,.bak" required>
<div class="form-text">Supported formats: .gz, .zip, .tar, .bak</div>
</div>
<div id="uploadPreview" class="mb-3" style="display: none;">
<div class="card bg-light">
<div class="card-body">
<h6>File Information</h6>
<div id="uploadPreviewContent">
<!-- File details will be shown here -->
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-warning">
<i class="fa fa-upload me-2"></i>Upload and Restore
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Restore Options -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Restore Options</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Pre-Restore Actions</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="createBackupBefore" checked>
<label class="form-check-label" for="createBackupBefore">
Create backup of current configuration
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="validateBackup" checked>
<label class="form-check-label" for="validateBackup">
Validate backup file before restore
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="checkDependencies">
<label class="form-check-label" for="checkDependencies">
Check system dependencies
</label>
</div>
</div>
<div class="col-md-6">
<h6>Post-Restore Actions</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="restartServices" checked>
<label class="form-check-label" for="restartServices">
Restart affected services
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="clearCache" checked>
<label class="form-check-label" for="clearCache">
Clear application cache
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="runMigrations">
<label class="form-check-label" for="runMigrations">
Run database migrations if needed
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="sendNotification" checked>
<label class="form-check-label" for="sendNotification">
Send completion notification
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Restore History -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Recent Restore History</h4>
<div class="card-tools">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="refreshRestoreHistory()">
<i class="fa fa-sync me-1"></i>Refresh
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Restore Date</th>
<th>Backup Source</th>
<th>Status</th>
<th>Duration</th>
<th>Restored By</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="restoreHistoryBody">
<tr>
<td colspan="6" class="text-center text-muted">
<i class="fa fa-spinner fa-spin me-2"></i>Loading restore history...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Restore Confirmation Modal -->
<div class="modal fade" id="restoreConfirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Configuration Restore</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger">
<i class="fa fa-exclamation-triangle me-2"></i>
<strong>Final Warning:</strong> This action will overwrite your current configuration and cannot be undone.
</div>
<div id="restoreConfirmDetails">
<!-- Restore details will be shown here -->
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="confirmRestore" required>
<label class="form-check-label" for="confirmRestore">
I understand the risks and want to proceed with the restore
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="confirmRestoreBtn" class="btn btn-danger" disabled>
<i class="fa fa-upload me-2"></i>Proceed with Restore
</button>
</div>
</div>
</div>
</div>
<!-- Restore Progress Modal -->
<div class="modal fade" id="restoreProgressModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Configuration Restore Progress</h5>
</div>
<div class="modal-body">
<div class="mb-3">
<div class="d-flex justify-content-between">
<span id="restoreProgressLabel">Initializing restore...</span>
<span id="restoreProgressPercent">0%</span>
</div>
<div class="progress">
<div id="restoreProgressBar" class="progress-bar" style="width: 0%"></div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="text-center">
<div id="currentStepIcon" class="mb-2">
<i class="fa fa-clock fa-2x text-muted"></i>
</div>
<div id="currentStepText" class="text-muted">Waiting...</div>
</div>
</div>
<div class="col-md-8">
<div id="restoreSteps">
<div class="restore-step" data-step="validate">
<i class="fa fa-check-circle text-muted me-2"></i>
<span>Validate backup file</span>
</div>
<div class="restore-step" data-step="backup">
<i class="fa fa-check-circle text-muted me-2"></i>
<span>Create current configuration backup</span>
</div>
<div class="restore-step" data-step="extract">
<i class="fa fa-check-circle text-muted me-2"></i>
<span>Extract backup contents</span>
</div>
<div class="restore-step" data-step="restore">
<i class="fa fa-check-circle text-muted me-2"></i>
<span>Restore configuration files</span>
</div>
<div class="restore-step" data-step="migrate">
<i class="fa fa-check-circle text-muted me-2"></i>
<span>Run database migrations</span>
</div>
<div class="restore-step" data-step="restart">
<i class="fa fa-check-circle text-muted me-2"></i>
<span>Restart services</span>
</div>
<div class="restore-step" data-step="verify">
<i class="fa fa-check-circle text-muted me-2"></i>
<span>Verify system health</span>
</div>
</div>
</div>
</div>
<div id="restoreLog" class="bg-dark text-light p-3" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.875rem;">
<!-- Restore log messages will appear here -->
</div>
</div>
<div class="modal-footer">
<button type="button" id="cancelRestoreBtn" class="btn btn-secondary">Cancel Restore</button>
<button type="button" id="closeRestoreProgressBtn" class="btn btn-primary" style="display: none;">Close</button>
</div>
</div>
</div>
</div>
<!-- Restore Details Modal -->
<div class="modal fade" id="restoreDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Restore Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="restoreDetailsContent">
<!-- Restore details will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script>
var currentRestoreId = null;
var selectedBackupData = null;
$(document).ready(function() {
loadAvailableBackups();
loadRestoreHistory();
setupEventHandlers();
});
function setupEventHandlers() {
// Form submissions
$('#restoreExistingForm').submit(function(e) {
e.preventDefault();
initiateRestore('existing');
});
$('#restoreUploadForm').submit(function(e) {
e.preventDefault();
initiateRestore('upload');
});
// Backup selection change
$('#existingBackupSelect').change(function() {
var backupId = $(this).val();
if (backupId) {
loadBackupPreview(backupId);
} else {
$('#backupPreview').hide();
}
});
// File input change
$('#backupFileInput').change(function() {
var file = this.files[0];
if (file) {
showFilePreview(file);
} else {
$('#uploadPreview').hide();
}
});
// Confirmation checkbox
$('#confirmRestore').change(function() {
$('#confirmRestoreBtn').prop('disabled', !$(this).is(':checked'));
});
// Confirm restore button
$('#confirmRestoreBtn').click(function() {
proceedWithRestore();
});
// Cancel restore
$('#cancelRestoreBtn').click(function() {
if (currentRestoreId && confirm('Are you sure you want to cancel the restore? This may leave the system in an inconsistent state.')) {
cancelRestore();
}
});
}
function loadAvailableBackups() {
$.get('{% url "core:get_backup_list" %}', function(data) {
var options = '<option value="">Select a backup to restore...</option>';
data.backups.forEach(function(backup) {
if (backup.status === 'completed') {
options += '<option value="' + backup.id + '">' +
backup.name + ' (' + formatDate(backup.created_at) + ') - ' +
formatFileSize(backup.size) + '</option>';
}
});
$('#existingBackupSelect').html(options);
}).fail(function() {
$('#existingBackupSelect').html('<option value="">Failed to load backups</option>');
});
}
function loadBackupPreview(backupId) {
$.get('{% url "core:get_backup_details" %}', {backup_id: backupId}, function(data) {
selectedBackupData = data;
var previewHtml = '<div class="row">' +
'<div class="col-md-6">' +
'<strong>Name:</strong> ' + data.name + '<br>' +
'<strong>Type:</strong> <span class="badge bg-primary">' + data.type + '</span><br>' +
'<strong>Size:</strong> ' + formatFileSize(data.size) + '<br>' +
'</div>' +
'<div class="col-md-6">' +
'<strong>Created:</strong> ' + formatDate(data.created_at) + '<br>' +
'<strong>Encrypted:</strong> ' + (data.encrypted ? 'Yes' : 'No') + '<br>' +
'<strong>Components:</strong> ' + (data.components || []).join(', ') + '<br>' +
'</div>' +
'</div>';
if (data.description) {
previewHtml += '<hr><strong>Description:</strong><br>' + data.description;
}
$('#backupPreviewContent').html(previewHtml);
$('#backupPreview').show();
});
}
function showFilePreview(file) {
var previewHtml = '<div class="row">' +
'<div class="col-md-6">' +
'<strong>File Name:</strong> ' + file.name + '<br>' +
'<strong>Size:</strong> ' + formatFileSize(file.size) + '<br>' +
'<strong>Type:</strong> ' + file.type + '<br>' +
'</div>' +
'<div class="col-md-6">' +
'<strong>Last Modified:</strong> ' + new Date(file.lastModified).toLocaleString() + '<br>' +
'</div>' +
'</div>';
$('#uploadPreviewContent').html(previewHtml);
$('#uploadPreview').show();
}
function initiateRestore(method) {
var confirmHtml = '';
if (method === 'existing') {
if (!selectedBackupData) {
toastr.error('Please select a backup to restore');
return;
}
confirmHtml = '<h6>Backup Information</h6>' +
'<ul>' +
'<li><strong>Name:</strong> ' + selectedBackupData.name + '</li>' +
'<li><strong>Type:</strong> ' + selectedBackupData.type + '</li>' +
'<li><strong>Created:</strong> ' + formatDate(selectedBackupData.created_at) + '</li>' +
'<li><strong>Size:</strong> ' + formatFileSize(selectedBackupData.size) + '</li>' +
'</ul>';
} else {
var file = $('#backupFileInput')[0].files[0];
if (!file) {
toastr.error('Please select a backup file to upload');
return;
}
confirmHtml = '<h6>File Information</h6>' +
'<ul>' +
'<li><strong>File Name:</strong> ' + file.name + '</li>' +
'<li><strong>Size:</strong> ' + formatFileSize(file.size) + '</li>' +
'<li><strong>Last Modified:</strong> ' + new Date(file.lastModified).toLocaleString() + '</li>' +
'</ul>';
}
confirmHtml += '<h6>Restore Options</h6><ul>';
if ($('#createBackupBefore').is(':checked')) {
confirmHtml += '<li>Create backup of current configuration</li>';
}
if ($('#validateBackup').is(':checked')) {
confirmHtml += '<li>Validate backup file before restore</li>';
}
if ($('#restartServices').is(':checked')) {
confirmHtml += '<li>Restart affected services</li>';
}
if ($('#clearCache').is(':checked')) {
confirmHtml += '<li>Clear application cache</li>';
}
if ($('#runMigrations').is(':checked')) {
confirmHtml += '<li>Run database migrations if needed</li>';
}
confirmHtml += '</ul>';
$('#restoreConfirmDetails').html(confirmHtml);
$('#confirmRestore').prop('checked', false);
$('#confirmRestoreBtn').prop('disabled', true);
$('#restoreConfirmModal').modal('show');
}
function proceedWithRestore() {
$('#restoreConfirmModal').modal('hide');
showRestoreProgress();
var formData = new FormData();
formData.append('csrfmiddlewaretoken', $('[name=csrfmiddlewaretoken]').val());
// Add restore options
formData.append('create_backup_before', $('#createBackupBefore').is(':checked'));
formData.append('validate_backup', $('#validateBackup').is(':checked'));
formData.append('check_dependencies', $('#checkDependencies').is(':checked'));
formData.append('restart_services', $('#restartServices').is(':checked'));
formData.append('clear_cache', $('#clearCache').is(':checked'));
formData.append('run_migrations', $('#runMigrations').is(':checked'));
formData.append('send_notification', $('#sendNotification').is(':checked'));
// Add backup source
if (selectedBackupData) {
formData.append('backup_id', selectedBackupData.id);
formData.append('restore_method', 'existing');
} else {
var file = $('#backupFileInput')[0].files[0];
formData.append('backup_file', file);
formData.append('restore_method', 'upload');
}
$.ajax({
url: '{% url "core:restore_configuration" %}',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(data) {
if (data.success) {
currentRestoreId = data.restore_id;
monitorRestoreProgress(data.restore_id);
} else {
hideRestoreProgress();
toastr.error('Failed to start restore: ' + data.error);
}
},
error: function() {
hideRestoreProgress();
toastr.error('Failed to start restore');
}
});
}
function showRestoreProgress() {
$('#restoreProgressLabel').text('Initializing restore...');
$('#restoreProgressPercent').text('0%');
$('#restoreProgressBar').css('width', '0%');
$('#restoreLog').html('');
$('#currentStepIcon').html('<i class="fa fa-clock fa-2x text-muted"></i>');
$('#currentStepText').text('Waiting...');
$('.restore-step i').removeClass('text-success text-danger').addClass('text-muted');
$('#cancelRestoreBtn').show();
$('#closeRestoreProgressBtn').hide();
$('#restoreProgressModal').modal('show');
}
function hideRestoreProgress() {
$('#restoreProgressModal').modal('hide');
}
function monitorRestoreProgress(restoreId) {
var checkProgress = function() {
$.get('{% url "core:get_restore_progress" %}', {restore_id: restoreId}, function(data) {
updateRestoreProgress(data.progress, data.message, data.log, data.current_step);
if (data.status === 'completed') {
$('#restoreProgressLabel').text('Restore completed successfully');
$('#currentStepIcon').html('<i class="fa fa-check-circle fa-2x text-success"></i>');
$('#currentStepText').text('Completed');
$('#cancelRestoreBtn').hide();
$('#closeRestoreProgressBtn').show();
toastr.success('Configuration restored successfully');
loadRestoreHistory();
} else if (data.status === 'failed') {
$('#restoreProgressLabel').text('Restore failed');
$('#currentStepIcon').html('<i class="fa fa-times-circle fa-2x text-danger"></i>');
$('#currentStepText').text('Failed');
$('#cancelRestoreBtn').hide();
$('#closeRestoreProgressBtn').show();
toastr.error('Restore failed: ' + data.error);
} else {
setTimeout(checkProgress, 2000);
}
}).fail(function() {
$('#restoreProgressLabel').text('Failed to get progress');
$('#currentStepIcon').html('<i class="fa fa-times-circle fa-2x text-danger"></i>');
$('#currentStepText').text('Error');
$('#cancelRestoreBtn').hide();
$('#closeRestoreProgressBtn').show();
});
};
checkProgress();
}
function updateRestoreProgress(progress, message, log, currentStep) {
$('#restoreProgressPercent').text(progress + '%');
$('#restoreProgressBar').css('width', progress + '%');
$('#restoreProgressLabel').text(message);
// Update current step
if (currentStep) {
$('#currentStepText').text(currentStep.charAt(0).toUpperCase() + currentStep.slice(1));
// Update step icons
$('.restore-step').each(function() {
var step = $(this).data('step');
var icon = $(this).find('i');
if (step === currentStep) {
icon.removeClass('text-muted text-success text-danger').addClass('text-warning');
icon.removeClass('fa-check-circle').addClass('fa-spinner fa-spin');
} else {
icon.removeClass('fa-spinner fa-spin').addClass('fa-check-circle');
}
});
}
if (log) {
var logHtml = $('#restoreLog').html();
logHtml += '<div>' + new Date().toLocaleTimeString() + ': ' + log + '</div>';
$('#restoreLog').html(logHtml);
$('#restoreLog').scrollTop($('#restoreLog')[0].scrollHeight);
}
}
function loadRestoreHistory() {
$.get('{% url "core:get_restore_history" %}', function(data) {
var html = '';
if (data.restores.length === 0) {
html = '<tr><td colspan="6" class="text-center text-muted">No restore history found</td></tr>';
} else {
data.restores.forEach(function(restore) {
html += '<tr>' +
'<td>' + formatDate(restore.created_at) + '</td>' +
'<td>' + restore.backup_name + '</td>' +
'<td><span class="badge bg-' + getStatusColor(restore.status) + '">' + restore.status + '</span></td>' +
'<td>' + (restore.duration || 'N/A') + '</td>' +
'<td>' + restore.restored_by + '</td>' +
'<td>' +
'<button class="btn btn-outline-primary btn-sm" onclick="viewRestoreDetails(\'' + restore.id + '\')">' +
'<i class="fa fa-eye"></i>' +
'</button>' +
'</td>' +
'</tr>';
});
}
$('#restoreHistoryBody').html(html);
}).fail(function() {
$('#restoreHistoryBody').html('<tr><td colspan="6" class="text-center text-danger">Failed to load restore history</td></tr>');
});
}
function viewRestoreDetails(restoreId) {
$.get('{% url "core:get_restore_details" %}', {restore_id: restoreId}, function(data) {
var detailsHtml = '<div class="row">' +
'<div class="col-md-6">' +
'<h6>Restore Information</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>Started:</strong></td><td>' + formatDate(data.created_at) + '</td></tr>' +
'<tr><td><strong>Completed:</strong></td><td>' + (data.completed_at ? formatDate(data.completed_at) : 'N/A') + '</td></tr>' +
'<tr><td><strong>Duration:</strong></td><td>' + (data.duration || 'N/A') + '</td></tr>' +
'<tr><td><strong>Status:</strong></td><td><span class="badge bg-' + getStatusColor(data.status) + '">' + data.status + '</span></td></tr>' +
'<tr><td><strong>Restored By:</strong></td><td>' + data.restored_by + '</td></tr>' +
'</table>' +
'</div>' +
'<div class="col-md-6">' +
'<h6>Backup Source</h6>' +
'<table class="table table-sm">' +
'<tr><td><strong>Backup Name:</strong></td><td>' + data.backup_name + '</td></tr>' +
'<tr><td><strong>Backup Type:</strong></td><td>' + data.backup_type + '</td></tr>' +
'<tr><td><strong>Backup Size:</strong></td><td>' + formatFileSize(data.backup_size) + '</td></tr>' +
'</table>' +
'</div>' +
'</div>';
if (data.log) {
detailsHtml += '<hr><h6>Restore Log</h6>' +
'<pre class="bg-light p-3" style="max-height: 300px; overflow-y: auto;">' + data.log + '</pre>';
}
if (data.error) {
detailsHtml += '<hr><h6>Error Details</h6>' +
'<div class="alert alert-danger">' + data.error + '</div>';
}
$('#restoreDetailsContent').html(detailsHtml);
$('#restoreDetailsModal').modal('show');
});
}
function refreshRestoreHistory() {
loadRestoreHistory();
toastr.success('Restore history refreshed');
}
function cancelRestore() {
$.post('{% url "core:cancel_restore" %}', {restore_id: currentRestoreId}, function(data) {
hideRestoreProgress();
if (data.success) {
toastr.info('Restore cancelled');
}
});
}
function getStatusColor(status) {
switch (status) {
case 'completed': return 'success';
case 'failed': return 'danger';
case 'in_progress': return 'warning';
case 'cancelled': return 'secondary';
default: return 'light';
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
var k = 1024;
var sizes = ['Bytes', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDate(dateString) {
return new Date(dateString).toLocaleString();
}
// Close progress modal
$('#closeRestoreProgressBtn').click(function() {
hideRestoreProgress();
});
</script>
<style>
.card-tools {
margin-left: auto;
}
.progress {
height: 20px;
}
.progress-bar {
transition: width 0.3s ease;
}
#restoreLog {
font-size: 0.875rem;
line-height: 1.4;
}
#restoreLog div {
margin-bottom: 2px;
}
.restore-step {
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
.restore-step:last-child {
border-bottom: none;
}
.restore-step i {
transition: all 0.3s ease;
}
.btn-group-sm .btn {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.table td {
vertical-align: middle;
}
.badge {
font-size: 0.75rem;
}
pre {
font-size: 0.875rem;
line-height: 1.4;
}
.alert-heading {
margin-bottom: 0.5rem;
}
.fa-2x {
font-size: 2em;
}
.fa-spin {
animation: fa-spin 2s infinite linear;
}
@keyframes fa-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
{% endblock %}