Files
quality_app-v2/app/templates/modules/settings/database_management.html
Quality App Developer 8de85ca87f feat: Implement warehouse module roles with auto-schema repair and remove module access section
- Add SchemaVerifier class for automatic database schema verification and repair
- Implement warehouse_manager (Level 75) and warehouse_worker (Level 35) roles
- Add zone-based access control for warehouse workers
- Implement worker-manager binding system with zone filtering
- Add comprehensive database auto-repair on Docker initialization
- Remove Module Access section from user form (role-based access only)
- Add autocomplete attributes to password fields for better UX
- Include detailed documentation for warehouse implementation
- Update initialize_db.py with schema verification as Step 0
2026-01-28 00:46:59 +02:00

1050 lines
48 KiB
HTML

{% extends "base.html" %}
{% block title %}Database Management{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/database_management.css') }}">
{% endblock %}
{% block content %}
<div class="container-fluid py-5">
<div class="row mb-4">
<div class="col-12">
<h1 class="mb-2">
<i class="fas fa-database"></i> Database Management
</h1>
<p class="text-muted mb-0">Backup, restore, and manage your database operations</p>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="list-group">
<a href="{{ url_for('settings.general_settings') }}" class="list-group-item list-group-item-action">
<i class="fas fa-sliders-h"></i> General Settings
</a>
<a href="{{ url_for('settings.user_management') }}" class="list-group-item list-group-item-action">
<i class="fas fa-users"></i> User Management
</a>
<a href="{{ url_for('settings.app_keys') }}" class="list-group-item list-group-item-action">
<i class="fas fa-key"></i> App Keys
</a>
<a href="{{ url_for('settings.database_management') }}" class="list-group-item list-group-item-action active">
<i class="fas fa-cogs"></i> Database Management
</a>
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
<i class="fas fa-database"></i> Database Info
</a>
<a href="{{ url_for('settings.logs_explorer') }}" class="list-group-item list-group-item-action">
<i class="fas fa-file-alt"></i> Logs Explorer
</a>
</div>
</div>
<div class="col-md-9">
<!-- Display Messages -->
{% if error %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle"></i> {{ error }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if success %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle"></i> {{ success }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<!-- Backup Retention Settings Section -->
<div class="card shadow-sm mb-4 border-info">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="fas fa-calendar-times"></i> Backup Retention Policy
</h5>
</div>
<div class="card-body">
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle"></i>
<strong>Automatic Cleanup:</strong> Set how long backups should be kept on the server. Older backups will be automatically deleted.
</div>
<div class="row">
<div class="col-md-8">
<form method="POST" action="{{ url_for('settings.save_backup_retention') }}" id="retention-form">
<div class="mb-3">
<label for="backup-retention-days" class="form-label">Keep Backups For:</label>
<div class="input-group">
<input type="number" class="form-control" id="backup-retention-days" name="retention_days" min="1" max="365" value="{{ backup_retention_days or 30 }}" required>
<span class="input-group-text">days</span>
</div>
<small class="text-muted d-block mt-2">
Backups older than this will be automatically deleted when retention policy is applied.
</small>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="auto-cleanup-enabled" name="auto_cleanup" value="1" {{ 'checked' if auto_cleanup else '' }}>
<label class="form-check-label" for="auto-cleanup-enabled">
Enable automatic cleanup of old backups
</label>
</div>
<small class="text-muted d-block mt-2">
When enabled, backups exceeding the retention period will be automatically deleted.
</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Save Retention Policy
</button>
</form>
</div>
<div class="col-md-4">
<div class="card bg-light border-info">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-trash"></i> Manual Cleanup</h6>
<p class="text-muted small mb-2">Delete backups older than the retention period right now:</p>
<button type="button" class="btn btn-warning btn-sm w-100" id="cleanup-old-backups-btn">
<i class="fas fa-broom"></i> Clean Up Old Backups
</button>
</div>
</div>
</div>
</div>
<!-- Scheduled Backups Section -->
<hr class="my-4">
<h6 class="mb-3"><i class="fas fa-clock"></i> Scheduled Backups</h6>
<p class="text-muted small mb-3">Create automatic backups on a schedule that respect your retention policy.</p>
<!-- New Schedule Form -->
<div class="card bg-light mb-3">
<div class="card-body">
<h6 class="card-title mb-3">Create New Schedule</h6>
<form id="schedule-form">
<div class="row">
<div class="col-md-6 mb-2">
<label for="schedule-name" class="form-label">Schedule Name:</label>
<input type="text" class="form-control" id="schedule-name" placeholder="e.g., Daily Backup" required>
</div>
<div class="col-md-6 mb-2">
<label for="schedule-frequency" class="form-label">Frequency:</label>
<select class="form-select" id="schedule-frequency" required onchange="toggleDayOfWeek()">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-2">
<label for="schedule-time" class="form-label">Time:</label>
<input type="time" class="form-control" id="schedule-time" value="02:00" required>
</div>
<div class="col-md-6 mb-2" id="day-of-week-container" style="display: none;">
<label for="schedule-day" class="form-label">Day of Week:</label>
<select class="form-select" id="schedule-day">
<option value="Monday">Monday</option>
<option value="Tuesday">Tuesday</option>
<option value="Wednesday">Wednesday</option>
<option value="Thursday">Thursday</option>
<option value="Friday">Friday</option>
<option value="Saturday">Saturday</option>
<option value="Sunday">Sunday</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-2">
<label for="schedule-type" class="form-label">Backup Type:</label>
<select class="form-select" id="schedule-type" required>
<option value="full">Full Database Backup</option>
<option value="data_only">Data Only Backup</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label d-block">&nbsp;</label>
<button type="button" class="btn btn-primary w-100" id="create-schedule-btn" onclick="saveBackupSchedule()">
<i class="fas fa-plus"></i> Create Schedule
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Active Schedules List -->
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Schedule Name</th>
<th>Frequency</th>
<th>Time</th>
<th>Type</th>
<th>Last Run</th>
<th>Next Run</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="schedules-list">
<tr>
<td colspan="8" class="text-center py-4">
<div class="empty-state-message">
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
<span>No scheduled backups configured</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Backup Management Section -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="fas fa-save"></i> Backup Management
</h5>
</div>
<div class="card-body">
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle"></i>
<strong>Backup Information:</strong> Create a complete backup of your database including structure and data. Backups are stored on the server.
</div>
<!-- Quick Actions -->
<div class="mb-4">
<div class="row g-2">
<div class="col-md-6">
<button type="button" class="btn btn-success w-100" id="backup-full-btn" data-bs-toggle="modal" data-bs-target="#backupModal" data-type="full">
<i class="fas fa-database"></i> Full Database Backup
</button>
<small class="text-muted d-block mt-2">Includes structure and all data</small>
</div>
<div class="col-md-6">
<button type="button" class="btn btn-info w-100" id="backup-data-btn" data-bs-toggle="modal" data-bs-target="#backupModal" data-type="data">
<i class="fas fa-box"></i> Data Only Backup
</button>
<small class="text-muted d-block mt-2">Data without table structure</small>
</div>
</div>
</div>
<!-- Storage Info -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card bg-light border-secondary">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-hdd"></i> Database Size</h6>
<div class="display-6" id="db-size">Loading...</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-light border-secondary">
<div class="card-body">
<h6 class="card-title"><i class="fas fa-clock"></i> Last Backup</h6>
<div class="display-6 small" id="last-backup">Loading...</div>
</div>
</div>
</div>
</div>
<!-- Recent Backups List -->
<h6 class="mb-3">Recent Backups</h6>
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Filename</th>
<th>Type</th>
<th>Size</th>
<th>Date Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="backups-list">
<tr>
<td colspan="5" class="text-center py-4">
<div class="empty-state-message">
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
<span>Loading backups...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Restore Management Section -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="fas fa-undo"></i> Restore Database
</h5>
</div>
<div class="card-body">
<div class="alert alert-warning mb-4">
<i class="fas fa-exclamation-triangle"></i>
<strong>Warning:</strong> Restoring a backup will <strong>overwrite</strong> your current database. This action cannot be undone. Always verify you're restoring the correct backup!
</div>
<div class="mb-3">
<label for="restore-backup-select" class="form-label">Select Backup to Restore:</label>
<select class="form-select" id="restore-backup-select">
<option value="">-- Select a backup --</option>
</select>
</div>
<div id="restore-info" style="display: none;" class="alert alert-info mb-3">
<p class="mb-2"><strong>Backup Details:</strong></p>
<p class="mb-0"><small id="restore-info-text"></small></p>
</div>
<button type="button" class="btn btn-warning" id="restore-btn" disabled>
<i class="fas fa-undo"></i> Restore from Backup
</button>
</div>
</div>
<!-- Table Truncate Section -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="fas fa-broom"></i> Clear Table Data
</h5>
</div>
<div class="card-body">
<div class="alert alert-danger mb-4">
<i class="fas fa-exclamation-circle"></i>
<strong>Caution:</strong> Clearing a table will <strong>delete all data</strong> from the selected table while preserving its structure. This action cannot be undone!
</div>
<div class="row">
<div class="col-md-6">
<label for="truncate-table-select" class="form-label">Select Table:</label>
<select class="form-select" id="truncate-table-select">
<option value="">-- Loading tables... --</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label d-block">&nbsp;</label>
<button type="button" class="btn btn-danger w-100" id="truncate-btn" disabled>
<i class="fas fa-trash"></i> Clear Selected Table
</button>
</div>
</div>
<div id="truncate-info" style="display: none;" class="alert alert-info mt-3 mb-0">
<p class="mb-2"><strong>Table Information:</strong></p>
<p class="mb-1"><strong>Name:</strong> <span id="truncate-table-name"></span></p>
<p class="mb-0"><strong>Rows to Delete:</strong> <span id="truncate-row-count" class="badge bg-danger"></span></p>
</div>
</div>
</div>
<!-- Import Data Section -->
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="fas fa-file-import"></i> Upload Backup File
</h5>
</div>
<div class="card-body">
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle"></i>
<strong>Upload:</strong> Upload SQL backup files (.sql) to store them alongside your automatic backups. You can then restore them using the Restore Database section.
</div>
<div class="mb-3">
<label for="import-file" class="form-label">Choose SQL File to Upload:</label>
<input type="file" class="form-control" id="import-file" accept=".sql" required>
<small class="text-muted d-block mt-2">Supported format: .sql files (e.g., from database exports)</small>
</div>
<button type="button" class="btn btn-primary" id="upload-backup-btn" onclick="uploadBackupFile()">
<i class="fas fa-upload"></i> Upload Backup File
</button>
<div id="upload-status" class="mt-3"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Backup Modal -->
<div class="modal fade" id="backupModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create Backup</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="backup-form" method="POST">
<div class="mb-3">
<label for="backup-name" class="form-label">Backup Name (Optional):</label>
<input type="text" class="form-control" id="backup-name" name="backup_name" placeholder="e.g., Pre-Migration Backup">
<small class="text-muted">If empty, a timestamp will be used</small>
</div>
<input type="hidden" name="backup_type" id="backup-type-input" value="">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="backup-form" class="btn btn-success">
<i class="fas fa-save"></i> Create Backup
</button>
</div>
</div>
</div>
</div>
<!-- Confirm Truncate Modal -->
<div class="modal fade" id="confirmTruncateModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content" style="background-color: var(--bg-primary); border-color: var(--border-color);">
<div class="modal-header" style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); border-bottom-color: var(--border-color);">
<h5 class="modal-title" style="color: white;"><i class="fas fa-exclamation-triangle"></i> Confirm Table Clear</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" style="color: var(--text-primary);">
<p class="mb-3">You are about to <strong>permanently delete all data</strong> from:</p>
<p class="p-3 rounded mb-3" style="background-color: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary);"><strong id="confirm-table-name" style="font-size: 1.1em; color: #ef4444;"></strong></p>
<div class="alert" role="alert" style="background-color: rgba(239, 68, 68, 0.1); border: 1px solid #ef4444; color: var(--text-primary);">
<i class="fas fa-exclamation-circle"></i> <strong>This action CANNOT be undone!</strong>
</div>
<p class="mb-2" style="color: var(--text-primary);"><strong>To confirm, please type the table name:</strong></p>
<input type="text" class="form-control" id="confirm-table-input" placeholder="Enter table name to confirm..." autocomplete="off" style="background-color: var(--input-bg); border-color: var(--input-border); color: var(--text-primary);">
<small class="text-muted d-block mt-2">This is a safety measure to prevent accidental data deletion.</small>
</div>
<div class="modal-footer" style="border-top-color: var(--border-color);">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" style="background-color: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-color);">Cancel</button>
<button type="button" class="btn btn-danger" id="confirm-truncate-btn" disabled style="background-color: #ef4444; border-color: #dc2626;">
<i class="fas fa-trash"></i> Yes, Clear All Data
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load all database tables dynamically
loadDatabaseTables();
// Load backups list
loadBackupsList();
// Load schedules list
loadBackupSchedules();
// ===== DECLARE VARIABLES ONCE AT THE TOP =====
const truncateSelect = document.getElementById('truncate-table-select');
const truncateBtn = document.getElementById('truncate-btn');
const confirmInput = document.getElementById('confirm-table-input');
const confirmBtn = document.getElementById('confirm-truncate-btn');
console.log('Initializing truncate handlers...');
console.log('truncateSelect:', truncateSelect);
console.log('truncateBtn:', truncateBtn);
console.log('confirmInput:', confirmInput);
console.log('confirmBtn:', confirmBtn);
// Cleanup old backups handler
const cleanupBtn = document.getElementById('cleanup-old-backups-btn');
if (cleanupBtn) {
cleanupBtn.addEventListener('click', function() {
if (confirm('This will delete all backups older than the retention period. Continue?')) {
fetch('{{ url_for("settings.cleanup_old_backups") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Cleanup completed! ' + (data.deleted_count || 0) + ' old backups deleted.');
loadBackupsList();
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Error cleaning up backups: ' + error);
});
}
});
}
// Backup button handlers
document.getElementById('backup-full-btn').addEventListener('click', function() {
document.getElementById('backup-type-input').value = 'full';
});
document.getElementById('backup-data-btn').addEventListener('click', function() {
document.getElementById('backup-type-input').value = 'data';
});
// Backup form submission
document.getElementById('backup-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('{{ url_for("settings.create_backup") }}', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Backup created successfully: ' + data.file);
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error creating backup: ' + error);
});
const modal = bootstrap.Modal.getInstance(document.getElementById('backupModal'));
if (modal) modal.hide();
});
// Truncate table SELECT change handler - enable/disable button
console.log('Registering SELECT change handler...');
if (truncateSelect && truncateBtn) {
truncateSelect.addEventListener('change', function() {
const table = this.value;
const option = this.options[this.selectedIndex];
console.log('=== TABLE SELECTED ===');
console.log('Selected table:', table);
if (table && table.trim()) {
console.log('✓ Valid table selected - ENABLING truncate button');
// Safely update info display
const truncateInfo = document.getElementById('truncate-info');
if (truncateInfo) {
truncateInfo.style.display = 'block';
}
// Safely update table name
const truncateTableName = document.getElementById('truncate-table-name');
if (truncateTableName) {
truncateTableName.textContent = table;
}
// Safely update row count
const truncateRowCount = document.getElementById('truncate-row-count');
if (truncateRowCount) {
truncateRowCount.textContent = option.text.match(/\((\d+)\s*rows\)/)?.[1] || '0';
}
// Safely update confirm table name
const confirmTableName = document.getElementById('confirm-table-name');
if (confirmTableName) {
confirmTableName.textContent = table;
}
// Enable the button
truncateBtn.disabled = false;
console.log('✓ Button enabled successfully');
} else {
console.log('✗ No table selected - DISABLING truncate button');
// Safely hide info display
const truncateInfo = document.getElementById('truncate-info');
if (truncateInfo) {
truncateInfo.style.display = 'none';
}
truncateBtn.disabled = true;
}
});
console.log('✓ Change event listener registered on truncate-table-select');
} else {
console.error('✗ truncate-table-select or truncate-btn not found!');
}
// Truncate button click handler - opens modal
if (truncateBtn && truncateSelect) {
truncateBtn.addEventListener('click', function() {
const selectedTable = truncateSelect.value;
console.log('Truncate button clicked, selected table:', selectedTable);
if (!selectedTable) {
alert('Please select a table first');
return;
}
// Show the modal
const modalElement = document.getElementById('confirmTruncateModal');
if (!modalElement) {
console.error('❌ confirmTruncateModal element not found');
alert('Error: Modal not found');
return;
}
const modal = new bootstrap.Modal(modalElement);
// Clear the input field
if (confirmInput) {
confirmInput.value = '';
}
// Disable the confirm button
if (confirmBtn) {
confirmBtn.disabled = true;
}
console.log('Opening modal for table:', selectedTable);
modal.show();
});
}
// Confirmation input handler - enables button only when user types correct table name
if (confirmInput && confirmBtn) {
confirmInput.addEventListener('input', function() {
const selectedTable = truncateSelect.value;
const inputValue = this.value.trim();
// Enable button only if user typed the exact table name
if (inputValue === selectedTable) {
confirmBtn.disabled = false;
console.log('✓ Table name matches - button enabled');
} else {
confirmBtn.disabled = true;
console.log('✗ Table name does not match - button disabled');
}
});
}
// Confirm truncate button click
if (confirmBtn) {
confirmBtn.addEventListener('click', function() {
const table = truncateSelect.value;
// Disable button to prevent multiple clicks
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Clearing...';
fetch('{{ url_for("settings.truncate_table") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ table: table })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Hide modal
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmTruncateModal'));
if (modal) modal.hide();
// Show success message
alert('Table cleared successfully! Refreshing page...');
// Refresh the page after a short delay
setTimeout(() => {
location.reload();
}, 500);
} else {
alert('Error: ' + data.error);
// Re-enable button
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash"></i> Yes, Clear All Data';
}
})
.catch(error => {
console.error('Error:', error);
alert('Error clearing table: ' + error);
// Re-enable button
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash"></i> Yes, Clear All Data';
});
});
}
// Restore backup handler
document.getElementById('restore-backup-select').addEventListener('change', function() {
const backup = this.value;
if (backup) {
document.getElementById('restore-info').style.display = 'block';
document.getElementById('restore-info-text').textContent = 'Selected: ' + backup;
document.getElementById('restore-btn').disabled = false;
} else {
document.getElementById('restore-info').style.display = 'none';
document.getElementById('restore-btn').disabled = true;
}
});
document.getElementById('restore-btn').addEventListener('click', function() {
if (confirm('Are you sure? This will overwrite your current database!')) {
const backup = document.getElementById('restore-backup-select').value;
fetch('{{ url_for("settings.restore_database") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ backup: backup })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Database restored successfully!');
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error restoring database: ' + error);
});
}
});
});
function loadBackupsList() {
fetch('{{ url_for("settings.get_backups_list") }}')
.then(response => response.json())
.then(data => {
if (data.backups && data.backups.length > 0) {
const tbody = document.getElementById('backups-list');
tbody.innerHTML = '';
data.backups.forEach(backup => {
const row = document.createElement('tr');
row.innerHTML = `
<td><code>${backup.name}</code></td>
<td><span class="badge bg-info">${backup.type}</span></td>
<td>${backup.size}</td>
<td><small>${backup.date}</small></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="downloadBackup('${backup.name}')">
<i class="fas fa-download"></i> Download
</button>
</td>
`;
tbody.appendChild(row);
});
// Update restore select
const select = document.getElementById('restore-backup-select');
select.innerHTML = '<option value="">-- Select a backup --</option>';
data.backups.forEach(backup => {
const option = document.createElement('option');
option.value = backup.name;
option.textContent = backup.name + ' (' + backup.size + ')';
select.appendChild(option);
});
// Update last backup info
if (data.backups.length > 0) {
document.getElementById('last-backup').textContent = data.backups[0].date;
}
} else {
document.getElementById('backups-list').innerHTML = `
<tr>
<td colspan="5" class="text-center py-4">
<div class="empty-state-message">
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
<span>No backups found. Create one now!</span>
</div>
</td>
</tr>
`;
}
// Update DB size
if (data.db_size) {
document.getElementById('db-size').textContent = data.db_size;
}
})
.catch(error => {
console.error('Error loading backups:', error);
document.getElementById('backups-list').innerHTML = `
<tr>
<td colspan="5" class="text-center py-4 text-danger">
<i class="fas fa-exclamation-circle"></i> Error loading backups
</td>
</tr>
`;
});
}
function downloadBackup(filename) {
window.location.href = '{{ url_for("settings.download_backup") }}?file=' + encodeURIComponent(filename);
}
// Schedule management functions
function toggleDayOfWeek() {
const frequency = document.getElementById('schedule-frequency').value;
const dayContainer = document.getElementById('day-of-week-container');
dayContainer.style.display = frequency === 'weekly' ? 'block' : 'none';
}
function loadBackupSchedules() {
fetch('{{ url_for("settings.get_backup_schedules") }}')
.then(response => response.json())
.then(data => {
const tbody = document.getElementById('schedules-list');
if (data.schedules && data.schedules.length > 0) {
tbody.innerHTML = '';
data.schedules.forEach(schedule => {
const lastRun = schedule.last_run ? new Date(schedule.last_run).toLocaleString() : 'Never';
const nextRun = schedule.next_run ? new Date(schedule.next_run).toLocaleString() : 'Calculating...';
const statusBadge = schedule.is_active ?
'<span class="badge bg-success">Active</span>' :
'<span class="badge bg-secondary">Inactive</span>';
const frequencyDisplay = schedule.frequency === 'daily' ? 'Daily' :
'Weekly (' + (schedule.day_of_week || 'Not set') + ')';
const typeDisplay = schedule.backup_type === 'full' ? 'Full Database' : 'Data Only';
const row = document.createElement('tr');
row.innerHTML = `
<td>${schedule.schedule_name}</td>
<td>${frequencyDisplay}</td>
<td>${schedule.time_of_day}</td>
<td><span class="badge bg-secondary">${typeDisplay}</span></td>
<td><small>${lastRun}</small></td>
<td><small>${nextRun}</small></td>
<td>${statusBadge}</td>
<td>
<button class="btn btn-sm btn-outline-warning" onclick="toggleSchedule(${schedule.id})" title="Enable/Disable">
<i class="fas fa-power-off"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteSchedule(${schedule.id})">
<i class="fas fa-trash"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
} else {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4">
<div class="empty-state-message">
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
<span>No scheduled backups configured</span>
</div>
</td>
</tr>
`;
}
})
.catch(error => {
console.error('Error loading schedules:', error);
});
}
function saveBackupSchedule() {
const name = document.getElementById('schedule-name').value.trim();
const frequency = document.getElementById('schedule-frequency').value;
const time = document.getElementById('schedule-time').value;
const dayOfWeek = document.getElementById('schedule-day').value;
const type = document.getElementById('schedule-type').value;
if (!name || !time) {
alert('Please fill in all required fields');
return;
}
if (frequency === 'weekly' && !dayOfWeek) {
alert('Please select a day for weekly schedules');
return;
}
const payload = {
schedule_name: name,
frequency: frequency,
day_of_week: frequency === 'weekly' ? dayOfWeek : null,
time_of_day: time,
backup_type: type
};
fetch('{{ url_for("settings.save_backup_schedule") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Backup schedule created successfully!');
document.getElementById('schedule-form').reset();
loadBackupSchedules();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error creating schedule: ' + error);
});
}
function deleteSchedule(scheduleId) {
if (confirm('Are you sure you want to delete this schedule?')) {
fetch('{{ url_for("settings.delete_backup_schedule", schedule_id=0) }}'.replace('0', scheduleId), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Schedule deleted successfully!');
loadBackupSchedules();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error deleting schedule: ' + error);
});
}
}
function toggleSchedule(scheduleId) {
fetch('{{ url_for("settings.toggle_backup_schedule", schedule_id=0) }}'.replace('0', scheduleId), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadBackupSchedules();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error toggling schedule: ' + error);
});
}
function uploadBackupFile() {
const fileInput = document.getElementById('import-file');
const file = fileInput.files[0];
if (!file) {
alert('Please select a file to upload');
return;
}
if (!file.name.toLowerCase().endsWith('.sql')) {
alert('Only .sql files are supported');
return;
}
const formData = new FormData();
formData.append('file', file);
const statusDiv = document.getElementById('upload-status');
statusDiv.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Uploading...</div>';
fetch('{{ url_for("settings.upload_backup_file") }}', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusDiv.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> File uploaded successfully: ' + data.filename + ' (' + data.size + ' bytes)</div>';
fileInput.value = '';
loadBackupsList();
} else {
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Upload failed: ' + data.error + '</div>';
}
})
.catch(error => {
console.error('Error:', error);
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Upload error: ' + error + '</div>';
});
}
// Load schedules on page load
function loadDatabaseTables() {
/**
* Dynamically load ALL database tables from the server
* This ensures that newly added tables are always available
* Called ONCE on page load
*/
console.log('🔄 Loading database tables...');
fetch('{{ url_for("settings.get_database_tables") }}')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success && data.tables && data.tables.length > 0) {
const select = document.getElementById('truncate-table-select');
if (!select) {
console.error('❌ truncate-table-select element not found');
return;
}
// Clear existing options except the first placeholder
select.innerHTML = '<option value="">-- Select a table --</option>';
// Add all tables as options
data.tables.forEach(table => {
const option = document.createElement('option');
option.value = table.name;
option.textContent = `${table.name} (${table.rows} rows)`;
select.appendChild(option);
});
console.log(`✓ Loaded ${data.tables.length} tables from database`);
} else {
console.warn('⚠️ No tables returned from server or API returned an error');
console.warn('Response:', data);
}
})
.catch(error => {
console.error('❌ Error loading database tables:', error);
document.getElementById('truncate-table-select').innerHTML = '<option value="">-- Error loading tables --</option>';
});
}
</script>
{% endblock %}