This commit is contained in:
2026-05-11 19:41:49 +05:00
parent 89e54538de
commit 75247d9821
4 changed files with 740 additions and 11 deletions

View File

@@ -340,6 +340,62 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
<h1>Настройки</h1>
</div>
<div class="card">
<div class="card-header-custom"><h3>Резервное копирование</h3></div>
<div class="card-body-custom">
<div class="row mb-4">
<div class="col-md-6">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="backupAutoEnabled" style="width: 50px; height: 24px; cursor: pointer;">
<label class="form-check-label fw-500 ms-2" for="backupAutoEnabled">Автоматическое резервное копирование</label>
</div>
</div>
<div class="row g-3">
<div class="col-auto">
<label class="form-label">Время запуска</label>
<input type="time" class="form-control" id="backupAutoTime" value="03:00" style="width: 130px;">
</div>
<div class="col-auto">
<label class="form-label">Хранить (дней)</label>
<input type="number" class="form-control" id="backupRetentionDays" value="30" min="1" max="365" style="width: 100px;">
</div>
<div class="col-auto d-flex align-items-end">
<button class="btn-gold btn-sm" onclick="saveBackupSettings()"><i class="fas fa-save me-1"></i>Сохранить</button>
</div>
</div>
</div>
<div class="col-md-6 d-flex align-items-center justify-content-end">
<button class="btn-primary-custom" onclick="createManualBackup()" id="btnCreateBackup">
<i class="fas fa-download me-2"></i>Создать бекап сейчас
</button>
</div>
</div>
<div class="card mt-3" style="background: #f8fafc; border: 1px solid #e2e8f0;">
<div class="card-body-custom">
<h4 class="mb-3" style="font-size: 1rem;"><i class="fas fa-history me-2"></i>История бекапов</h4>
<div style="max-height: 300px; overflow-y: auto;">
<table class="table table-sm">
<thead>
<tr>
<th>Дата</th>
<th>Размер</th>
<th>Тип</th>
<th>Восстановлен</th>
<th class="text-end">Действия</th>
</tr>
</thead>
<tbody id="backupsTableBody">
<tr><td colspan="5" class="text-center text-muted">Загрузка...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header-custom"><h3>Кодовое слово для отзывов</h3></div>
<div class="card-body-custom">
<p style="color: #64748b; margin-bottom: 20px;">
@@ -368,17 +424,6 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
</div>
</div>
</div>
<div id="tab-profile" class="tab-content">
<div class="top-bar"><h1>Мой профиль</h1></div>
<div class="card">
<div class="card-body-custom">
<div class="profile-section">
<div class="profile-avatar" id="profileAvatar"></div>
<div class="profile-info">
<h4 id="profileName"></h4>
<p id="profileLogin"></p>
</div>
</div>
<form id="profileForm">
<div class="row g-3">
@@ -474,6 +519,34 @@ tr.row-checkout-today { background: #fef2f2 !important; border-left: 4px solid #
</div>
</div>
<div class="modal-backdrop-custom" id="restoreModal">
<div class="modal-custom">
<div class="modal-header-custom">
<h3><i class="fas fa-exclamation-triangle text-warning me-2"></i>Восстановление из бекапа</h3>
<button class="modal-close" onclick="hideRestoreModal()">&times;</button>
</div>
<div class="modal-body-custom">
<div class="alert alert-warning" style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 10px;">
<p class="mb-2"><strong>Внимание!</strong></p>
<p class="mb-2">Текущие данные будут заменены данными из бекапа.</p>
<p class="mb-0"><strong>Перед восстановлением будет создан аварийный бекап текущего состояния.</strong></p>
</div>
<div class="mt-3">
<p class="mb-1"><strong>Бэкап:</strong> <span id="restoreBackupFilename"></span></p>
<p class="mb-1"><strong>Дата создания:</strong> <span id="restoreBackupDate"></span></p>
<p class="mb-0"><strong>Размер:</strong> <span id="restoreBackupSize"></span></p>
</div>
<input type="hidden" id="restoreBackupId">
</div>
<div class="modal-footer-custom">
<button type="button" class="btn btn-secondary btn-sm" onclick="hideRestoreModal()">Отмена</button>
<button type="button" class="btn-danger-custom" onclick="confirmRestore()" id="btnConfirmRestore">
<i class="fas fa-undo me-1"></i>Восстановить
</button>
</div>
</div>
</div>
<div class="modal-backdrop-custom" id="promocodeModal">
<div class="modal-custom">
<div class="modal-header-custom">
@@ -1170,6 +1243,13 @@ async function loadSettings() {
const display = document.getElementById('currentCodeDisplay');
if (display) display.value = 'Ошибка загрузки';
}
try {
await loadBackupSettings();
await loadBackupsList();
} catch(err) {
console.error('Failed to load backup module:', err);
}
}
document.getElementById('settingsForm').addEventListener('submit', async e => {
@@ -1199,6 +1279,197 @@ function toggleCodeShow() {
}
}
// Backup Functions
async function loadBackupSettings() {
try {
const settings = await api('/api/admin/backup/settings');
document.getElementById('backupAutoEnabled').checked = settings.backup_auto_enabled === 'true';
document.getElementById('backupAutoTime').value = settings.backup_auto_time || '03:00';
document.getElementById('backupRetentionDays').value = settings.backup_retention_days || '30';
} catch(err) {
console.error('Failed to load backup settings:', err);
}
}
async function saveBackupSettings() {
const enabled = document.getElementById('backupAutoEnabled').checked;
const time = document.getElementById('backupAutoTime').value;
const retention = document.getElementById('backupRetentionDays').value;
try {
await api('/api/admin/backup/settings', {
method: 'PUT',
body: JSON.stringify({
backup_auto_enabled: enabled,
backup_auto_time: time,
backup_retention_days: retention
})
});
showToast('Настройки резервного копирования сохранены');
} catch(err) {
showToast('Ошибка сохранения настроек: ' + err.message, 'error');
}
}
async function createManualBackup() {
const btn = document.getElementById('btnCreateBackup');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="loading-spinner"></span> Создание...';
try {
const result = await api('/api/admin/backup', { method: 'POST' });
showToast('Бекап создан: ' + result.backup.filename);
loadBackupsList();
} catch(err) {
showToast('Ошибка создания бекапа: ' + err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
async function loadBackupsList() {
try {
const result = await api('/api/admin/backups');
const tbody = document.getElementById('backupsTableBody');
if (!result.backups || result.backups.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">Нет бекапов</td></tr>';
return;
}
tbody.innerHTML = result.backups.map(b => {
const date = new Date(b.created_at);
const dateStr = date.toLocaleDateString('ru-RU') + ' ' + date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
const sizeStr = formatBackupSize(b.size);
const typeLabel = b.type === 'auto' ? '<span class="badge" style="background: #dbeafe; color: #1e40af;">авто</span>' :
b.type === 'emergency_before_restore' ? '<span class="badge" style="background: #fef3c7; color: #92400e;">аварийный</span>' :
'<span class="badge" style="background: #dcfce7; color: #15803d;">ручной</span>';
const restoredStr = b.restored_at ? new Date(b.restored_at).toLocaleDateString('ru-RU') : '—';
return '<tr>' +
'<td style="white-space: nowrap;">' + esc(dateStr) + '</td>' +
'<td>' + sizeStr + '</td>' +
'<td>' + typeLabel + '</td>' +
'<td>' + esc(restoredStr) + '</td>' +
'<td class="text-end">' +
'<button class="btn btn-outline-secondary btn-sm me-1" onclick="downloadBackup(' + b.id + ')" title="Скачать"><i class="fas fa-download"></i></button>' +
'<button class="btn btn-outline-warning btn-sm me-1" onclick="showRestoreModal(' + b.id + ')" title="Восстановить"><i class="fas fa-undo"></i></button>' +
'<button class="btn-danger-custom btn-sm" onclick="deleteBackupConfirm(' + b.id + ')" title="Удалить"><i class="fas fa-trash"></i></button>' +
'</td></tr>';
}).join('');
} catch(err) {
const tbody = document.getElementById('backupsTableBody');
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Ошибка загрузки</td></tr>';
}
}
function formatBackupSize(bytes) {
if (!bytes) return '—';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
let backupToDelete = null;
function deleteBackupConfirm(id) {
backupToDelete = id;
if (confirm('Удалить этот бекап?')) {
doDeleteBackup();
}
}
async function doDeleteBackup() {
if (!backupToDelete) return;
try {
await api('/api/admin/backups/' + backupToDelete, { method: 'DELETE' });
showToast('Бекап удалён');
loadBackupsList();
} catch(err) {
showToast('Ошибка удаления: ' + err.message, 'error');
} finally {
backupToDelete = null;
}
}
async function downloadBackup(id) {
try {
const response = await fetch('/api/admin/backups/' + id + '/download', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (!response.ok) throw new Error('Download failed');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const contentDisposition = response.headers.get('Content-Disposition');
if (contentDisposition) {
const match = contentDisposition.match(/filename="?(.+)"?/);
if (match) a.download = match[1];
}
if (!a.download) a.download = 'backup.db';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch(err) {
showToast('Ошибка скачивания: ' + err.message, 'error');
}
}
let backupToRestore = null;
async function showRestoreModal(id) {
try {
const result = await api('/api/admin/backups');
const backup = result.backups.find(b => b.id === id);
if (!backup) {
showToast('Бекап не найден', 'error');
return;
}
backupToRestore = backup;
document.getElementById('restoreBackupFilename').textContent = backup.filename;
document.getElementById('restoreBackupDate').textContent = new Date(backup.created_at).toLocaleString('ru-RU');
document.getElementById('restoreBackupSize').textContent = formatBackupSize(backup.size);
document.getElementById('restoreModal').classList.add('show');
} catch(err) {
showToast('Ошибка загрузки данных бекапа', 'error');
}
}
function hideRestoreModal() {
document.getElementById('restoreModal').classList.remove('show');
backupToRestore = null;
}
async function confirmRestore() {
if (!backupToRestore) return;
const btn = document.getElementById('btnConfirmRestore');
btn.disabled = true;
btn.innerHTML = '<span class="loading-spinner"></span> Восстановление...';
try {
const result = await api('/api/admin/backups/' + backupToRestore.id + '/restore', { method: 'POST' });
hideRestoreModal();
showToast('Восстановление завершено! Перезагрузите страницу.');
setTimeout(() => {
if (confirm('Данные были восстановлены. Перезагрузить страницу?')) {
window.location.reload();
}
}, 1000);
} catch(err) {
showToast('Ошибка восстановления: ' + err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-undo me-1"></i>Восстановить';
}
}
// ===== ROOMS MODULE =====
const ROOM_TYPES_LIST = ['2x-местный', '3х-местный', 'Семейный', 'Люкс'];