Files
quality_app/py_app/app/templates/settings.html
Quality System Admin 9c19379810 updated backups solution
2025-11-03 22:18:56 +02:00

796 lines
31 KiB
HTML
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Settings{% endblock %}
{% block content %}
<div class="card-container">
<div class="card">
<h3>Manage Users (Legacy)</h3>
<ul class="user-list">
{% for user in users %}
<li data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">
<span class="user-name">{{ user.username }}</span>
<span class="user-role">Role: {{ user.role }}</span>
<button class="btn edit-user-btn" data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">Edit User</button>
<button class="btn delete-btn delete-user-btn" data-user-id="{{ user.id }}" data-username="{{ user.username }}">Delete User</button>
</li>
{% endfor %}
</ul>
<button id="create-user-btn" class="btn create-btn">Create User</button>
</div>
<div class="card">
<h3>External Server Settings</h3>
<form method="POST" action="{{ url_for('main.save_external_db') }}" class="form-centered">
<label for="db_server_domain">Server Domain/IP Address:</label>
<input type="text" id="db_server_domain" name="server_domain" value="{{ external_settings.get('server_domain', '') }}" required>
<label for="db_port">Port:</label>
<input type="number" id="db_port" name="port" value="{{ external_settings.get('port', '') }}" required>
<label for="db_database_name">Database Name:</label>
<input type="text" id="db_database_name" name="database_name" value="{{ external_settings.get('database_name', '') }}" required>
<label for="db_username">Username:</label>
<input type="text" id="db_username" name="username" value="{{ external_settings.get('username', '') }}" required>
<label for="db_password">Password:</label>
<input type="password" id="db_password" name="password" value="{{ external_settings.get('password', '') }}" required>
<button type="submit" class="btn">Save/Update External Database Info Settings</button>
</form>
</div>
<div class="card" style="margin-top: 32px;">
<h3>🎯 User & Permissions Management</h3>
<p><strong>Simplified 4-Tier System:</strong> Superadmin → Admin → Manager → Worker</p>
<p>Streamlined interface with module-based permissions (Quality, Warehouse, Labels)</p>
<div style="margin-top: 15px;">
<a href="{{ url_for('main.user_management_simple') }}" class="btn" style="background-color: #2196f3; color: white; margin-right: 10px;">
🎯 Manage Users (Simplified)
</a>
</div>
<small style="display: block; margin-top: 10px; color: #666;">
Recommended: Use the simplified user management for easier administration
</small>
</div>
{% if session.role in ['superadmin', 'admin'] %}
<div class="card backup-card" style="margin-top: 32px;">
<h3>💾 Database Backup Management</h3>
<p><strong>Automated Backup System:</strong> Schedule and manage database backups</p>
<!-- Backup Controls -->
<div class="backup-controls">
<h4 style="margin-top: 0;">Quick Actions</h4>
<button id="backup-now-btn" class="btn" style="background-color: #4caf50; color: white; margin-right: 10px;">
⚡ Backup Now
</button>
<button id="refresh-backups-btn" class="btn" style="background-color: #2196f3; color: white;">
🔄 Refresh List
</button>
</div>
<!-- Schedule Configuration -->
<div class="backup-schedule">
<h4 style="margin-top: 0;">Backup Schedule</h4>
<form id="backup-schedule-form" class="schedule-form">
<div>
<label for="schedule-enabled">
<input type="checkbox" id="schedule-enabled" name="enabled"> Enable Scheduled Backups
</label>
</div>
<div>
<label for="schedule-time">Backup Time:</label>
<input type="time" id="schedule-time" name="time" value="02:00">
</div>
<div>
<label for="schedule-frequency">Frequency:</label>
<select id="schedule-frequency" name="frequency">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div>
<label for="retention-days">Keep backups for (days):</label>
<input type="number" id="retention-days" name="retention_days" value="30" min="1" max="365">
</div>
<div style="grid-column: span 2;">
<button type="submit" class="btn" style="background-color: #ff9800; color: white;">
💾 Save Schedule
</button>
</div>
</form>
</div>
<!-- Backup List -->
<div style="margin-top: 20px;">
<h4>Available Backups</h4>
<div id="backup-list" class="backup-list-container">
<p style="text-align: center; color: #999;">Loading backups...</p>
</div>
</div>
<!-- Backup Path Info -->
<div class="backup-info">
<strong> Backup Location:</strong> <code id="backup-path-display">/srv/quality_app/backups</code>
<br>
<small>Configure backup path in docker-compose.yml (BACKUP_PATH environment variable)</small>
</div>
<!-- Restore Database Section (Superadmin Only) -->
{% if current_user.role == 'superadmin' %}
<div class="restore-section" style="margin-top: 30px; padding: 20px; border: 2px solid #ff9800; border-radius: 8px; background: #fff3e0;">
<h4 style="margin: 0 0 10px 0; color: #e65100;">⚠️ Restore Database</h4>
<p style="margin: 0 0 15px 0; color: #e65100; font-weight: bold;">
WARNING: Restoring will permanently replace ALL current data with the backup data. This action cannot be undone!
</p>
<!-- Upload External Backup File -->
<div style="margin-bottom: 20px; padding: 15px; background: #e3f2fd; border: 1px solid #2196f3; border-radius: 4px;">
<h5 style="margin: 0 0 10px 0; color: #1976d2;">📤 Upload External Backup File</h5>
<p style="margin: 0 0 10px 0; font-size: 0.9em; color: #555;">
Upload a backup file from another server or external source. File will be saved to the backups directory.
</p>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="file" id="backup-file-upload" accept=".sql" style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
<button id="upload-backup-btn" class="btn" style="background: #2196f3; color: white; padding: 10px 20px; white-space: nowrap;">
⬆️ Upload File
</button>
</div>
<small style="color: #666; display: block; margin-top: 5px;">
Accepted format: .sql files only | Max size: 100MB
</small>
</div>
<!-- Select Backup to Restore -->
<div style="margin-bottom: 15px;">
<label for="restore-backup-select" style="display: block; margin-bottom: 5px; font-weight: bold;">Select Backup to Restore:</label>
<select id="restore-backup-select" style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px;">
<option value="">-- Select a backup file --</option>
</select>
</div>
<button id="restore-btn" class="btn" style="background: #ff5722; color: white; padding: 10px 20px; width: 100%; font-weight: bold;" disabled>
🔄 Restore Database from Selected Backup
</button>
</div>
{% endif %}
</div>
<style>
/* Backup Card Styles */
.backup-card {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.backup-controls {
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.backup-schedule {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
}
.schedule-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.schedule-form label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.schedule-form input[type="time"],
.schedule-form input[type="number"],
.schedule-form select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
color: #333;
}
.backup-list-container {
max-height: 400px;
overflow-y: auto;
background: #fafafa;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
}
.backup-list-container table {
background: #fff;
}
.backup-list-container th {
background: #f0f0f0;
color: #333;
}
.backup-list-container tr {
border-bottom: 1px solid #ddd;
}
.backup-info {
margin-top: 15px;
padding: 10px;
background: #e3f2fd;
border-left: 4px solid #2196f3;
border-radius: 4px;
color: #0d47a1;
}
.backup-info code {
background: #bbdefb;
padding: 2px 6px;
border-radius: 3px;
color: #01579b;
}
.backup-info small {
color: #1565c0;
}
/* Dark Mode Styles */
body.dark-mode .backup-card {
background: #2d2d2d;
color: #e0e0e0;
}
body.dark-mode .backup-controls {
background: #3a3a3a;
}
body.dark-mode .backup-schedule {
background: #353535;
}
body.dark-mode .schedule-form input[type="time"],
body.dark-mode .schedule-form input[type="number"],
body.dark-mode .schedule-form select {
background: #2d2d2d;
color: #e0e0e0;
border-color: #555;
}
body.dark-mode .schedule-form label {
color: #e0e0e0;
}
body.dark-mode .backup-list-container {
background: #2d2d2d;
border-color: #555;
}
body.dark-mode .backup-list-container table {
background: #2d2d2d;
color: #e0e0e0;
}
body.dark-mode .backup-list-container th {
background: #3a3a3a;
color: #e0e0e0;
}
body.dark-mode .backup-list-container tr {
border-bottom-color: #555;
}
body.dark-mode .backup-list-container td {
color: #e0e0e0;
}
body.dark-mode .backup-info {
background: #1e3a5f;
border-left-color: #2196f3;
color: #90caf9;
}
body.dark-mode .backup-info code {
background: #2d4a6d;
color: #64b5f6;
}
body.dark-mode .backup-info small {
color: #81c784;
}
/* Dark mode for restore section */
body.dark-mode .restore-section {
background: #3a2a1f !important;
border-color: #ff9800 !important;
}
body.dark-mode .restore-section h4,
body.dark-mode .restore-section p {
color: #ffb74d !important;
}
body.dark-mode .restore-section label {
color: #e0e0e0;
}
body.dark-mode .restore-section select {
background: #2d2d2d;
color: #e0e0e0;
border-color: #555;
}
/* Dark mode for upload section */
body.dark-mode .restore-section div[style*="background: #e3f2fd"] {
background: #1a3a52 !important;
border-color: #2196f3 !important;
}
body.dark-mode .restore-section h5 {
color: #64b5f6 !important;
}
body.dark-mode .restore-section input[type="file"] {
background: #2d2d2d;
color: #e0e0e0;
border-color: #555;
}
</style>
{% endif %}
</div>
<!-- Popup for creating/editing a user -->
<div id="user-popup" class="popup" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; background:var(--app-overlay-bg, rgba(30,41,59,0.85)); z-index:9999; align-items:center; justify-content:center;">
<div class="popup-content" style="margin:auto; padding:32px; border-radius:8px; box-shadow:0 2px 8px #333; min-width:320px; max-width:400px; text-align:center;">
<h3 id="user-popup-title">Create/Edit User</h3>
<form id="user-form" method="POST" action="{{ url_for('main.create_user') }}">
<input type="hidden" id="user-id" name="user_id">
<label for="user_username">Username:</label>
<input type="text" id="user_username" name="username" required>
<label for="user_email">Email (Optional):</label>
<input type="email" id="user_email" name="email">
<label for="user_password">Password:</label>
<input type="password" id="user_password" name="password" required>
<label for="user_role">Role:</label>
<select id="user_role" name="role" required>
<option value="superadmin">Superadmin</option>
<option value="admin">Admin</option>
<option value="manager">Manager</option>
<option value="warehouse_manager">Warehouse Manager</option>
<option value="warehouse_worker">Warehouse Worker</option>
<option value="quality_manager">Quality Manager</option>
<option value="quality_worker">Quality Worker</option>
</select>
<button type="submit" class="btn">Save</button>
<button type="button" id="close-user-popup-btn" class="btn cancel-btn">Cancel</button>
</form>
</div>
</div>
<!-- Popup for confirming user deletion -->
<div id="delete-user-popup" class="popup">
<div class="popup-content">
<h3>Do you really want to delete the user <span id="delete-username"></span>?</h3>
<form id="delete-user-form" method="POST" action="{{ url_for('main.delete_user') }}">
<input type="hidden" id="delete-user-id" name="user_id">
<button type="submit" class="btn delete-confirm-btn">Yes</button>
<button type="button" id="close-delete-popup-btn" class="btn cancel-btn">No</button>
</form>
</div>
</div>
<script>
document.getElementById('create-user-btn').onclick = function() {
document.getElementById('user-popup').style.display = 'flex';
document.getElementById('user-popup-title').innerText = 'Create User';
document.getElementById('user-form').reset();
document.getElementById('user-form').setAttribute('action', '{{ url_for("main.create_user") }}');
document.getElementById('user-id').value = '';
document.getElementById('user_password').required = true;
document.getElementById('user_password').placeholder = '';
document.getElementById('user_username').readOnly = false;
};
document.getElementById('close-user-popup-btn').onclick = function() {
document.getElementById('user-popup').style.display = 'none';
};
// Edit User button logic
Array.from(document.getElementsByClassName('edit-user-btn')).forEach(function(btn) {
btn.onclick = function() {
document.getElementById('user-popup').style.display = 'flex';
document.getElementById('user-popup-title').innerText = 'Edit User';
document.getElementById('user-id').value = btn.getAttribute('data-user-id');
document.getElementById('user_username').value = btn.getAttribute('data-username');
document.getElementById('user_email').value = btn.getAttribute('data-email') || '';
document.getElementById('user_role').value = btn.getAttribute('data-role');
document.getElementById('user_password').value = '';
document.getElementById('user_password').required = false;
document.getElementById('user_password').placeholder = 'Leave blank to keep current password';
document.getElementById('user_username').readOnly = true;
document.getElementById('user-form').setAttribute('action', '{{ url_for("main.edit_user") }}');
};
});
// Delete User button logic
Array.from(document.getElementsByClassName('delete-user-btn')).forEach(function(btn) {
btn.onclick = function() {
document.getElementById('delete-user-popup').style.display = 'flex';
document.getElementById('delete-username').innerText = btn.getAttribute('data-username');
document.getElementById('delete-user-id').value = btn.getAttribute('data-user-id');
};
});
document.getElementById('close-delete-popup-btn').onclick = function() {
document.getElementById('delete-user-popup').style.display = 'none';
};
// ========================================
// Database Backup Management Functions
// ========================================
// Load backup schedule on page load
function loadBackupSchedule() {
fetch('/api/backup/schedule')
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('schedule-enabled').checked = data.schedule.enabled;
document.getElementById('schedule-time').value = data.schedule.time;
document.getElementById('schedule-frequency').value = data.schedule.frequency;
document.getElementById('retention-days').value = data.schedule.retention_days;
}
})
.catch(error => console.error('Error loading schedule:', error));
}
// Load backup list
function loadBackupList() {
const backupList = document.getElementById('backup-list');
if (!backupList) return;
backupList.innerHTML = '<p style="text-align: center; color: #999;">Loading backups...</p>';
fetch('/api/backup/list')
.then(response => response.json())
.then(data => {
if (data.success && data.backups.length > 0) {
let html = '<table style="width: 100%; border-collapse: collapse;">';
html += '<thead><tr style="background: #f0f0f0;"><th style="padding: 10px; text-align: left;">Filename</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead>';
html += '<tbody>';
data.backups.forEach(backup => {
html += `<tr style="border-bottom: 1px solid #ddd;">
<td style="padding: 10px;">${backup.filename}</td>
<td style="text-align: center;">${backup.size_mb} MB</td>
<td style="text-align: center;">${backup.created}</td>
<td style="text-align: center;">
<button onclick="downloadBackup('${backup.filename}')" class="btn" style="background: #2196f3; color: white; padding: 5px 10px; margin: 2px;">⬇️ Download</button>
<button onclick="deleteBackup('${backup.filename}')" class="btn" style="background: #f44336; color: white; padding: 5px 10px; margin: 2px;">🗑️ Delete</button>
</td>
</tr>`;
});
html += '</tbody></table>';
backupList.innerHTML = html;
// Populate restore dropdown
const restoreSelect = document.getElementById('restore-backup-select');
if (restoreSelect) {
restoreSelect.innerHTML = '<option value="">-- Select a backup file --</option>';
data.backups.forEach(backup => {
restoreSelect.innerHTML += `<option value="${backup.filename}">${backup.filename} (${backup.size_mb} MB - ${backup.created})</option>`;
});
}
} else {
backupList.innerHTML = '<p style="text-align: center; color: #999;">No backups available</p>';
// Clear restore dropdown
const restoreSelect = document.getElementById('restore-backup-select');
if (restoreSelect) {
restoreSelect.innerHTML = '<option value="">-- No backups available --</option>';
}
}
})
.catch(error => {
console.error('Error loading backups:', error);
backupList.innerHTML = '<p style="text-align: center; color: #f44336;">Error loading backups</p>';
});
}
// Backup now button
document.getElementById('backup-now-btn')?.addEventListener('click', function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = '⏳ Creating backup...';
fetch('/api/backup/create', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message + '\nFile: ' + data.filename + '\nSize: ' + data.size);
loadBackupList();
} else {
alert('❌ ' + data.message);
}
btn.disabled = false;
btn.innerHTML = '⚡ Backup Now';
})
.catch(error => {
console.error('Error creating backup:', error);
alert('❌ Failed to create backup');
btn.disabled = false;
btn.innerHTML = '⚡ Backup Now';
});
});
// Refresh backups button
document.getElementById('refresh-backups-btn')?.addEventListener('click', function() {
loadBackupList();
});
// Save schedule form
document.getElementById('backup-schedule-form')?.addEventListener('submit', function(e) {
e.preventDefault();
const formData = {
enabled: document.getElementById('schedule-enabled').checked,
time: document.getElementById('schedule-time').value,
frequency: document.getElementById('schedule-frequency').value,
retention_days: parseInt(document.getElementById('retention-days').value)
};
fetch('/api/backup/schedule', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
} else {
alert('❌ ' + data.message);
}
})
.catch(error => {
console.error('Error saving schedule:', error);
alert('❌ Failed to save schedule');
});
});
// Download backup function
function downloadBackup(filename) {
window.location.href = `/api/backup/download/${filename}`;
}
// Delete backup function
function deleteBackup(filename) {
if (confirm(`Are you sure you want to delete backup: ${filename}?`)) {
fetch(`/api/backup/delete/${filename}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
loadBackupList();
} else {
alert('❌ ' + data.message);
}
})
.catch(error => {
console.error('Error deleting backup:', error);
alert('❌ Failed to delete backup');
});
}
}
// Restore dropdown change - enable/disable button
document.getElementById('restore-backup-select')?.addEventListener('change', function() {
const restoreBtn = document.getElementById('restore-btn');
if (this.value) {
restoreBtn.disabled = false;
} else {
restoreBtn.disabled = true;
}
});
// Restore backup function
document.getElementById('restore-btn')?.addEventListener('click', function() {
const filename = document.getElementById('restore-backup-select').value;
if (!filename) {
alert('❌ Please select a backup file to restore');
return;
}
// First confirmation
const firstConfirm = confirm(
`⚠️ CRITICAL WARNING ⚠️\n\n` +
`You are about to RESTORE the database from:\n${filename}\n\n` +
`This will PERMANENTLY DELETE all current data and replace it with the backup data.\n\n` +
`This action CANNOT be undone!\n\n` +
`Do you want to continue?`
);
if (!firstConfirm) {
return;
}
// Second confirmation (require typing confirmation)
const secondConfirm = prompt(
`⚠️ FINAL CONFIRMATION ⚠️\n\n` +
`Type "RESTORE" in capital letters to confirm you understand:\n` +
`• All current database data will be PERMANENTLY DELETED\n` +
`• This action is IRREVERSIBLE\n` +
`• Users may experience downtime during restore\n\n` +
`Type RESTORE to continue:`
);
if (secondConfirm !== 'RESTORE') {
alert('❌ Restore cancelled - confirmation text did not match');
return;
}
// Perform restore
const btn = this;
btn.disabled = true;
btn.innerHTML = '⏳ Restoring database... Please wait...';
fetch(`/api/backup/restore/${filename}`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(
`✅ DATABASE RESTORE COMPLETE!\n\n` +
`${data.message}\n\n` +
`The application will now reload to apply changes.`
);
// Reload the page to ensure all data is fresh
window.location.reload();
} else {
alert(`❌ RESTORE FAILED\n\n${data.message}`);
btn.disabled = false;
btn.innerHTML = '🔄 Restore Database from Selected Backup';
}
})
.catch(error => {
console.error('Error restoring backup:', error);
alert(`❌ RESTORE FAILED\n\nAn error occurred while restoring the database.`);
btn.disabled = false;
btn.innerHTML = '🔄 Restore Database from Selected Backup';
});
});
// Upload backup file
document.getElementById('upload-backup-btn')?.addEventListener('click', function() {
const fileInput = document.getElementById('backup-file-upload');
const file = fileInput.files[0];
if (!file) {
alert('❌ Please select a file to upload');
return;
}
// Validate file extension
if (!file.name.toLowerCase().endsWith('.sql')) {
alert('❌ Invalid file format. Only .sql files are allowed.');
return;
}
// Validate file size (10GB max for large databases)
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB in bytes
if (file.size > maxSize) {
alert('❌ File is too large. Maximum size is 10GB.');
return;
}
// Warn about large files
const warningSize = 1 * 1024 * 1024 * 1024; // 1GB
if (file.size > warningSize) {
const sizeGB = (file.size / (1024 * 1024 * 1024)).toFixed(2);
if (!confirm(`⚠️ Large File Warning\n\nYou are uploading a ${sizeGB} GB file.\nThis may take several minutes.\n\nDo you want to continue?`)) {
return;
}
}
// Prepare form data
const formData = new FormData();
formData.append('backup_file', file);
// Disable button and show loading
const btn = this;
btn.disabled = true;
btn.innerHTML = '⏳ Uploading and validating...';
// Upload file
fetch('/api/backup/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Build detailed success message with validation info
let message = `✅ File uploaded and validated successfully!\n\n`;
message += `Filename: ${data.filename}\n`;
message += `Size: ${data.size}\n`;
// Add validation details if available
if (data.validation && data.validation.details) {
const details = data.validation.details;
message += `\n📊 Validation Results:\n`;
message += `• Lines: ${details.line_count || 'N/A'}\n`;
message += `• Has Users Table: ${details.has_users_table ? '✓' : '✗'}\n`;
message += `• Has Data: ${details.has_insert_statements ? '✓' : '✗'}\n`;
}
// Add warnings if any
if (data.validation && data.validation.warnings && data.validation.warnings.length > 0) {
message += `\n⚠️ Warnings:\n`;
data.validation.warnings.forEach(warning => {
message += `${warning}\n`;
});
}
message += `\nThe file is now available in the restore dropdown.`;
alert(message);
// Clear file input
fileInput.value = '';
// Reload backup list to show the new file
loadBackupList();
} else {
// Build detailed error message
let message = `❌ Upload failed\n\n${data.message}`;
// Add validation details if available
if (data.validation_details) {
message += `\n\n📊 Validation Details:\n`;
const details = data.validation_details;
if (details.size_mb) message += `• File Size: ${details.size_mb} MB\n`;
if (details.line_count) message += `• Lines: ${details.line_count}\n`;
}
// Add warnings if any
if (data.warnings && data.warnings.length > 0) {
message += `\n⚠️ Issues Found:\n`;
data.warnings.forEach(warning => {
message += `${warning}\n`;
});
}
alert(message);
}
btn.disabled = false;
btn.innerHTML = '⬆️ Upload File';
})
.catch(error => {
console.error('Error uploading backup:', error);
alert('❌ Failed to upload file');
btn.disabled = false;
btn.innerHTML = '⬆️ Upload File';
});
});
// Load backup data on page load
if (document.getElementById('backup-list')) {
loadBackupSchedule();
loadBackupList();
}
</script>
{% endblock %}