Update backup system and settings UI
- Improved backup path handling for Docker environments - Changed default backup type to data-only for scheduled backups - Updated settings.html with enhanced backup management UI - Replaced 'Drop Table' with 'Truncate Table' functionality - Clear data while preserving structure and triggers - Changed from danger zone styling (red) to caution styling (orange) - Added clear confirmation dialog with table name verification - Added upload backup file functionality - Improved backup schedule management with edit/toggle/delete - Updated styling and dark mode support - Removed old backup metadata files
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"schedules": [
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default Schedule",
|
||||
"enabled": true,
|
||||
"time": "03:00",
|
||||
"frequency": "daily",
|
||||
"backup_type": "data-only",
|
||||
"retention_days": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
[
|
||||
{
|
||||
"filename": "data_only_test_20251105_190632.sql",
|
||||
"size": 305541,
|
||||
"timestamp": "2025-11-05T19:06:32.251145",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251106_030000.sql",
|
||||
"size": 305632,
|
||||
"timestamp": "2025-11-06T03:00:00.179220",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251107_030000.sql",
|
||||
"size": 325353,
|
||||
"timestamp": "2025-11-07T03:00:00.178234",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251108_030000.sql",
|
||||
"size": 346471,
|
||||
"timestamp": "2025-11-08T03:00:00.175266",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251109_030000.sql",
|
||||
"size": 364071,
|
||||
"timestamp": "2025-11-09T03:00:00.175309",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251110_030000.sql",
|
||||
"size": 364071,
|
||||
"timestamp": "2025-11-10T03:00:00.174557",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251111_030000.sql",
|
||||
"size": 392102,
|
||||
"timestamp": "2025-11-11T03:00:00.175496",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251112_030000.sql",
|
||||
"size": 417468,
|
||||
"timestamp": "2025-11-12T03:00:00.177699",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_trasabilitate_20251113_002851.sql",
|
||||
"size": 435126,
|
||||
"timestamp": "2025-11-13T00:28:51.949113",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "backup_trasabilitate_20251113_004522.sql",
|
||||
"size": 455459,
|
||||
"timestamp": "2025-11-13T00:45:22.992984",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251113_030000.sql",
|
||||
"size": 435126,
|
||||
"timestamp": "2025-11-13T03:00:00.187954",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251114_030000.sql",
|
||||
"size": 458259,
|
||||
"timestamp": "2025-11-14T03:00:00.179754",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251115_030000.sql",
|
||||
"size": 484020,
|
||||
"timestamp": "2025-11-15T03:00:00.181883",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251116_030000.sql",
|
||||
"size": 494281,
|
||||
"timestamp": "2025-11-16T03:00:00.179753",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251117_030000.sql",
|
||||
"size": 494281,
|
||||
"timestamp": "2025-11-17T03:00:00.181115",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251118_030000.sql",
|
||||
"size": 536395,
|
||||
"timestamp": "2025-11-18T03:00:00.183002",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251119_030000.sql",
|
||||
"size": 539493,
|
||||
"timestamp": "2025-11-19T03:00:00.182323",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251120_030000.sql",
|
||||
"size": 539493,
|
||||
"timestamp": "2025-11-20T03:00:00.182801",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251121_030000.sql",
|
||||
"size": 539493,
|
||||
"timestamp": "2025-11-21T03:00:00.183179",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251122_030000.sql",
|
||||
"size": 539493,
|
||||
"timestamp": "2025-11-22T03:00:00.182628",
|
||||
"database": "trasabilitate"
|
||||
},
|
||||
{
|
||||
"filename": "data_only_scheduled_20251227_030000.sql",
|
||||
"size": 16038,
|
||||
"timestamp": "2025-12-27T03:00:00.088164",
|
||||
"database": "trasabilitate"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -78,14 +78,21 @@ class DatabaseBackupManager:
|
||||
return None
|
||||
|
||||
def _get_backup_path(self):
|
||||
"""Get backup path from environment or use default"""
|
||||
# Check environment variable (set in docker-compose)
|
||||
backup_path = os.environ.get('BACKUP_PATH', '/srv/quality_app/backups')
|
||||
"""Get backup path - use container path when in Docker"""
|
||||
# When running in Docker container, use the mounted container path
|
||||
# The volume is always mounted at /srv/quality_app/backups in the container
|
||||
# regardless of the host path specified in BACKUP_PATH env var
|
||||
if os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER'):
|
||||
# Running in Docker - use container path
|
||||
backup_path = '/srv/quality_app/backups'
|
||||
else:
|
||||
# Running on host - use environment variable or default
|
||||
backup_path = os.environ.get('BACKUP_PATH', '/srv/quality_app/backups')
|
||||
|
||||
# Check if custom path is set in config
|
||||
# Check if custom path is set in config (host deployments)
|
||||
try:
|
||||
settings_file = os.path.join(current_app.instance_path, 'external_server.conf')
|
||||
if os.path.exists(settings_file):
|
||||
if os.path.exists(settings_file) and not (os.path.exists('/.dockerenv') or os.environ.get('DOCKER_CONTAINER')):
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith('backup_path='):
|
||||
@@ -672,7 +679,7 @@ class DatabaseBackupManager:
|
||||
'enabled': False,
|
||||
'time': '02:00', # 2 AM
|
||||
'frequency': 'daily', # daily, weekly, monthly
|
||||
'backup_type': 'full', # full or data-only
|
||||
'backup_type': 'data-only', # full or data-only
|
||||
'retention_days': 30 # Keep backups for 30 days
|
||||
}
|
||||
|
||||
|
||||
@@ -144,19 +144,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Database Table Management Section -->
|
||||
<div style="margin-bottom: 24px; padding: 20px; background: var(--sub-card-bg, rgba(0,0,0,0.02)); border: 1px solid var(--border-color, rgba(0,0,0,0.1)); border-radius: 8px; border-left: 4px solid #f44336;">
|
||||
<div style="margin-bottom: 24px; padding: 20px; background: var(--sub-card-bg, rgba(0,0,0,0.02)); border: 1px solid var(--border-color, rgba(0,0,0,0.1)); border-radius: 8px; border-left: 4px solid #ff9800;">
|
||||
<h4 style="margin: 0 0 15px 0; color: var(--text-primary, #333); display: flex; align-items: center; gap: 8px;">
|
||||
<span>🗑️ Database Table Management</span>
|
||||
<span style="background: #ff5722; color: white; font-size: 0.65em; padding: 3px 8px; border-radius: 4px; font-weight: 600;">DANGER ZONE</span>
|
||||
<span>🧹 Database Table Management</span>
|
||||
<span style="background: #ff9800; color: white; font-size: 0.65em; padding: 3px 8px; border-radius: 4px; font-weight: 600;">CAUTION</span>
|
||||
</h4>
|
||||
|
||||
<div style="padding: 12px 16px; background: var(--warning-bg, rgba(255, 87, 34, 0.1)); border-left: 4px solid #ff5722; border-radius: 4px; margin-bottom: 20px;">
|
||||
<div style="padding: 12px 16px; background: var(--warning-bg, rgba(255, 152, 0, 0.1)); border-left: 4px solid #ff9800; border-radius: 4px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||
<span style="font-size: 1.2em;">⚠️</span>
|
||||
<strong style="color: var(--warning-text, #d84315); font-size: 1.05em;">Warning</strong>
|
||||
<span style="font-size: 1.2em;">ℹ️</span>
|
||||
<strong style="color: var(--warning-text, #e65100); font-size: 1.05em;">Clear Table Data</strong>
|
||||
</div>
|
||||
<p style="margin: 0; color: var(--text-secondary, #666); font-size: 0.9em; line-height: 1.6;">
|
||||
Dropping tables will <strong>permanently delete all data</strong> in the selected table. This action cannot be undone. Always create a backup before dropping tables!
|
||||
Clearing a table will <strong>delete all data</strong> from the selected table while preserving its structure and all associated functions. This action cannot be undone. Always create a backup before clearing data!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -169,21 +169,21 @@
|
||||
<div id="tables-list-container" style="display: none;">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary, #666);">
|
||||
Select table to drop:
|
||||
Select table to clear:
|
||||
</label>
|
||||
<select id="table-to-drop" style="width: 100%; padding: 10px 14px; border: 1px solid var(--input-border, #ddd); border-radius: 6px; font-size: 1em; background: var(--input-bg, white); color: var(--text-primary, #333);">
|
||||
<select id="table-to-truncate" style="width: 100%; padding: 10px 14px; border: 1px solid var(--input-border, #ddd); border-radius: 6px; font-size: 1em; background: var(--input-bg, white); color: var(--text-primary, #333);">
|
||||
<option value="">-- Select a table --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="table-info" style="display: none; margin-bottom: 15px; padding: 12px; background: var(--info-bg-alt, rgba(33, 150, 243, 0.1)); border-radius: 6px; font-size: 0.9em;">
|
||||
<div style="margin-bottom: 5px;"><strong>Table:</strong> <span id="info-table-name"></span></div>
|
||||
<div style="margin-bottom: 5px;"><strong>Rows:</strong> <span id="info-row-count"></span></div>
|
||||
<div><strong>Size:</strong> <span id="info-table-size"></span></div>
|
||||
<div style="margin-bottom: 5px;"><strong>Rows to Clear:</strong> <span id="info-row-count"></span></div>
|
||||
<div><strong>Structure:</strong> <span style="color: #4caf50; font-weight: 600;">✓ Will be preserved</span></div>
|
||||
</div>
|
||||
|
||||
<button id="drop-table-btn" class="btn" style="background-color: #f44336; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: all 0.3s;" disabled>
|
||||
🗑️ Drop Selected Table
|
||||
<button id="truncate-table-btn" class="btn" style="background-color: #ff9800; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: all 0.3s;" disabled>
|
||||
🧹 Clear Selected Table
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -229,11 +229,11 @@
|
||||
<h5 style="margin: 0 0 12px 0; font-size: 0.95em; color: var(--text-primary, #333); display: flex; align-items: center; gap: 6px;">
|
||||
<span style="color: #4caf50;">💾</span> Backup Single Table
|
||||
</h5>
|
||||
<div style="padding: 16px; background: var(--input-bg, white); border: 1px solid var(--input-border, #ddd); border-radius: 6px;">
|
||||
<div style="padding: 16px; background: var(--card-bg, #fff); border: 1px solid var(--border-color, #ddd); border-radius: 6px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.85em; color: var(--text-secondary, #666);">
|
||||
Select Table:
|
||||
</label>
|
||||
<select id="table-backup-select" style="width: 100%; padding: 10px; border: 1px solid var(--input-border, #ddd); border-radius: 4px; margin-bottom: 12px; background: var(--input-bg, white); color: var(--text-primary, #333);">
|
||||
<select id="table-backup-select" style="width: 100%; padding: 10px; border: 1px solid var(--border-color, #ddd); border-radius: 4px; margin-bottom: 12px; background: var(--card-bg, #fff); color: var(--text-primary, #333);">
|
||||
<option value="">-- Select table to backup --</option>
|
||||
</select>
|
||||
<button id="backup-single-table-btn" class="compact-btn" style="width: 100%; background: #4caf50; color: white; padding: 10px;" disabled>
|
||||
@@ -247,11 +247,11 @@
|
||||
<h5 style="margin: 0 0 12px 0; font-size: 0.95em; color: var(--text-primary, #333); display: flex; align-items: center; gap: 6px;">
|
||||
<span style="color: #ff9800;">🔄</span> Restore Single Table
|
||||
</h5>
|
||||
<div style="padding: 16px; background: var(--input-bg, white); border: 1px solid var(--input-border, #ddd); border-radius: 6px;">
|
||||
<div style="padding: 16px; background: var(--card-bg, #fff); border: 1px solid var(--border-color, #ddd); border-radius: 6px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.85em; color: var(--text-secondary, #666);">
|
||||
Select Backup:
|
||||
</label>
|
||||
<select id="table-restore-backup-select" style="width: 100%; padding: 10px; border: 1px solid var(--input-border, #ddd); border-radius: 4px; margin-bottom: 12px; background: var(--input-bg, white); color: var(--text-primary, #333);">
|
||||
<select id="table-restore-backup-select" style="width: 100%; padding: 10px; border: 1px solid var(--border-color, #ddd); border-radius: 4px; margin-bottom: 12px; background: var(--card-bg, #fff); color: var(--text-primary, #333);">
|
||||
<option value="">-- Select backup to restore --</option>
|
||||
</select>
|
||||
<button id="restore-single-table-btn" class="compact-btn" style="width: 100%; background: #ff9800; color: white; padding: 10px;" disabled>
|
||||
@@ -276,8 +276,17 @@
|
||||
<h4 style="margin: 0; font-size: 0.95em; font-weight: 600; color: var(--text-color, #333);">➕ New Schedule</h4>
|
||||
</div>
|
||||
<div class="sub-card-body" style="padding: 12px;">
|
||||
<!-- Hint Message (shown when form is hidden) -->
|
||||
<div id="schedule-form-hint" style="padding: 16px; text-align: center; color: var(--text-color, #333); background: rgba(76, 175, 80, 0.08); border-radius: 4px; border-left: 4px solid #4caf50;">
|
||||
<p style="margin: 0; font-size: 0.9em; line-height: 1.6;">
|
||||
<strong style="color: #4caf50;">Press the ➕ button</strong> from the<br>
|
||||
<strong style="color: var(--text-color, #333);">⏰ Active Schedules</strong> card<br>
|
||||
to create a new schedule
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Schedule Form -->
|
||||
<form id="backup-schedule-form" class="schedule-compact-form" style="font-size: 0.9em;">
|
||||
<form id="backup-schedule-form" class="schedule-compact-form" style="font-size: 0.9em; display: none;">
|
||||
<input type="hidden" id="schedule-id" name="id">
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
@@ -351,7 +360,13 @@
|
||||
<div class="backup-sub-card" style="background: var(--sub-card-bg, #fafafa); border-radius: 6px; overflow: hidden; border: 1px solid var(--sub-card-border, #e0e0e0);">
|
||||
<div class="sub-card-header" style="background: var(--sub-header-bg, #f5f5f5); padding: 10px 12px; border-bottom: 1px solid var(--sub-card-border, #e0e0e0); display: flex; justify-content: space-between; align-items: center;">
|
||||
<h4 style="margin: 0; font-size: 0.95em; font-weight: 600; color: var(--text-color, #333);">📂 Backups</h4>
|
||||
<span id="backup-count-badge" style="background: #2196f3; color: white; font-size: 0.75em; padding: 3px 8px; border-radius: 10px; font-weight: 600;">0</span>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<span id="backup-count-badge" style="background: #2196f3; color: white; font-size: 0.75em; padding: 3px 8px; border-radius: 10px; font-weight: 600;">0</span>
|
||||
<button id="upload-backup-btn" class="btn-small" style="background: #4caf50; color: white; padding: 4px 10px; border-radius: 4px; font-size: 0.8em; border: none; cursor: pointer; font-weight: 600;">
|
||||
➕
|
||||
</button>
|
||||
<input type="file" id="backup-file-input" style="display: none;" accept=".sql,.gz" multiple>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-card-body" style="padding: 12px; max-height: 300px; overflow-y: auto;">
|
||||
<div id="backup-list" class="backup-list-modern">
|
||||
@@ -367,43 +382,54 @@
|
||||
|
||||
<!-- Full Database Restore Section (Superadmin Only) -->
|
||||
{% if session.role == 'superadmin' %}
|
||||
<div style="grid-column: 1 / -1; margin-top: 16px; padding: 16px; background: var(--warning-bg, rgba(255, 87, 34, 0.1)); border: 1px solid #ff5722; border-radius: 8px;">
|
||||
<h4 style="margin: 0 0 12px 0; color: var(--text-primary, #333); display: flex; align-items: center; gap: 8px;">
|
||||
<span>🔄 Full Database Restore</span>
|
||||
<span style="background: #ff5722; color: white; font-size: 0.65em; padding: 3px 8px; border-radius: 4px; font-weight: 600;">SUPERADMIN</span>
|
||||
</h4>
|
||||
<div style="grid-column: 1 / -1; margin-top: 16px;">
|
||||
<!-- Restore Card -->
|
||||
<div style="padding: 20px; background: var(--warning-bg, rgba(255, 87, 34, 0.1)); border: 1px solid #ff5722; border-radius: 8px; margin-bottom: 12px;">
|
||||
<h4 style="margin: 0 0 16px 0; color: var(--text-primary, #333); display: flex; align-items: center; gap: 8px;">
|
||||
<span>🔄 Full Database Restore</span>
|
||||
<span style="background: #ff5722; color: white; font-size: 0.65em; padding: 3px 8px; border-radius: 4px; font-weight: 600;">SUPERADMIN</span>
|
||||
</h4>
|
||||
|
||||
<div style="padding: 10px 12px; background: var(--warning-bg, rgba(255, 87, 34, 0.15)); border-left: 4px solid #ff5722; border-radius: 4px; margin-bottom: 12px; font-size: 0.85em;">
|
||||
<strong>⚠️ Warning:</strong> This will replace ALL current data. Cannot be undone!
|
||||
<div style="padding: 12px; background: var(--warning-bg, rgba(255, 87, 34, 0.15)); border-left: 4px solid #ff5722; border-radius: 4px; margin-bottom: 16px; font-size: 0.85em;">
|
||||
<strong>⚠️ Warning:</strong> This will replace ALL current data. Cannot be undone!
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 0.9em; color: var(--text-secondary, #666);">
|
||||
Select Backup:
|
||||
</label>
|
||||
<select id="restore-backup-select" style="width: 100%; padding: 10px; border: 1px solid var(--border-color, #ddd); border-radius: 4px; background: var(--input-bg, #fff); color: var(--text-primary, #333); font-size: 0.9em; margin-bottom: 12px;">
|
||||
<option value="">-- No backups available --</option>
|
||||
</select>
|
||||
<button id="restore-btn" class="compact-btn" style="width: 100%; background: #ff5722; color: white; font-size: 0.9em; padding: 12px;" disabled>
|
||||
🔄 Restore
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px; background: rgba(0,0,0,0.02); border-radius: 4px;">
|
||||
<p style="margin: 0 0 12px 0; font-weight: 600; font-size: 0.85em; color: var(--text-secondary, #666);">Restore Type:</p>
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 0.9em; margin-bottom: 8px;">
|
||||
<input type="radio" name="restore-type" value="full" checked>
|
||||
<span>Full Restore (schema + data)</span>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 0.9em;">
|
||||
<input type="radio" name="restore-type" value="data-only">
|
||||
<span>Data-Only (keep schema)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 12px; margin-bottom: 12px;">
|
||||
<select id="restore-backup-select" style="padding: 8px; border: 1px solid var(--input-border, #ddd); border-radius: 4px; background: var(--input-bg, white); color: var(--text-primary, #333); font-size: 0.9em;">
|
||||
<option value="">-- Select backup to restore --</option>
|
||||
</select>
|
||||
<button id="restore-btn" class="compact-btn" style="background: #ff5722; color: white; font-size: 0.9em;" disabled>
|
||||
🔄 Restore
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 0.85em;">
|
||||
<input type="radio" name="restore-type" value="full" checked>
|
||||
<span>Full Restore (schema + data)</span>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 0.85em;">
|
||||
<input type="radio" name="restore-type" value="data-only">
|
||||
<span>Data-Only (keep schema)</span>
|
||||
</label>
|
||||
<!-- Backup Location Card -->
|
||||
<div style="padding: 16px; background: var(--card-bg, #fff); border: 1px solid var(--border-color, #ddd); border-radius: 8px; border-left: 4px solid #4caf50;">
|
||||
<h5 style="margin: 0 0 12px 0; color: var(--text-primary, #333); font-size: 0.95em; display: flex; align-items: center; gap: 6px;">
|
||||
<span>📂 Backup Location</span>
|
||||
</h5>
|
||||
<code id="backup-location-path" style="display: block; padding: 12px; background: rgba(76, 175, 80, 0.08); border: 1px solid rgba(76, 175, 80, 0.2); border-radius: 4px; font-size: 0.85em; color: var(--text-primary, #333); word-break: break-all; font-family: 'Courier New', monospace;">Loading...</code>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Info -->
|
||||
<div style="grid-column: 1 / -1; margin-top: 12px; padding: 10px; background: var(--info-bg, rgba(76, 175, 80, 0.1)); border-left: 4px solid #4caf50; border-radius: 4px; font-size: 0.85em;">
|
||||
<strong>💾 Location:</strong> <code style="background: var(--code-bg, rgba(0,0,0,0.05)); padding: 2px 6px; border-radius: 3px;">/srv/quality_app/backups</code>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
@@ -1119,6 +1145,12 @@
|
||||
--sub-card-border: #555;
|
||||
}
|
||||
|
||||
body.dark-mode [style*="border-left: 4px solid #4caf50"] {
|
||||
background: #2d2d2d !important;
|
||||
border-color: #555 !important;
|
||||
--card-bg: #2d2d2d;
|
||||
}
|
||||
|
||||
body.dark-mode .sub-card-header {
|
||||
background: #444;
|
||||
border-bottom-color: #555;
|
||||
@@ -1313,6 +1345,16 @@
|
||||
--next-run-time: #c8e6c9;
|
||||
}
|
||||
|
||||
body.dark-mode #schedule-form-hint {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: #e0e0e0;
|
||||
border-left-color: #66bb6a;
|
||||
}
|
||||
|
||||
body.dark-mode #schedule-form-hint strong {
|
||||
color: #81c784;
|
||||
}
|
||||
|
||||
body.dark-mode .btn-icon-small {
|
||||
background: #444;
|
||||
border-color: #555;
|
||||
@@ -1395,15 +1437,18 @@
|
||||
|
||||
/* Select dropdown dark mode */
|
||||
body.dark-mode #log-retention-days,
|
||||
body.dark-mode #table-to-drop {
|
||||
background: rgba(255,255,255,0.05);
|
||||
body.dark-mode #table-to-truncate,
|
||||
body.dark-mode #restore-backup-select {
|
||||
background: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
border-color: rgba(255,255,255,0.2);
|
||||
border-color: #555;
|
||||
--input-bg: #3a3a3a;
|
||||
}
|
||||
|
||||
body.dark-mode #log-retention-days option,
|
||||
body.dark-mode #table-to-drop option {
|
||||
background: #2a2a2a;
|
||||
body.dark-mode #table-to-truncate option,
|
||||
body.dark-mode #restore-backup-select option {
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
@@ -1413,12 +1458,12 @@
|
||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
|
||||
}
|
||||
|
||||
body.dark-mode #drop-table-btn:hover:not(:disabled) {
|
||||
background-color: #d32f2f !important;
|
||||
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.4);
|
||||
body.dark-mode #truncate-table-btn:hover:not(:disabled) {
|
||||
background-color: #f57c00 !important;
|
||||
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4);
|
||||
}
|
||||
|
||||
body.dark-mode #drop-table-btn:disabled {
|
||||
body.dark-mode #truncate-table-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -1880,6 +1925,14 @@ document.getElementById('backup-single-table-btn')?.addEventListener('click', fu
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = confirm(
|
||||
'💾 BACKUP TABLE?\n\n' +
|
||||
'Table: ' + tableName + '\n\n' +
|
||||
'Are you sure you want to create a backup of this table?'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const btn = this;
|
||||
const originalText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
@@ -2005,7 +2058,7 @@ document.getElementById('load-tables-btn')?.addEventListener('click', function()
|
||||
|
||||
if (data.success && data.tables.length > 0) {
|
||||
tablesData = data.tables;
|
||||
const select = document.getElementById('table-to-drop');
|
||||
const select = document.getElementById('table-to-truncate');
|
||||
select.innerHTML = '<option value="">-- Select a table --</option>';
|
||||
|
||||
data.tables.forEach(table => {
|
||||
@@ -2027,9 +2080,9 @@ document.getElementById('load-tables-btn')?.addEventListener('click', function()
|
||||
});
|
||||
|
||||
// Table selection change
|
||||
document.getElementById('table-to-drop')?.addEventListener('change', function() {
|
||||
document.getElementById('table-to-truncate')?.addEventListener('change', function() {
|
||||
const tableName = this.value;
|
||||
const dropBtn = document.getElementById('drop-table-btn');
|
||||
const truncateBtn = document.getElementById('truncate-table-btn');
|
||||
const infoDiv = document.getElementById('table-info');
|
||||
|
||||
if (tableName) {
|
||||
@@ -2037,19 +2090,18 @@ document.getElementById('table-to-drop')?.addEventListener('change', function()
|
||||
if (tableData) {
|
||||
document.getElementById('info-table-name').textContent = tableData.name;
|
||||
document.getElementById('info-row-count').textContent = tableData.rows;
|
||||
document.getElementById('info-table-size').textContent = tableData.size;
|
||||
infoDiv.style.display = 'block';
|
||||
dropBtn.disabled = false;
|
||||
truncateBtn.disabled = false;
|
||||
}
|
||||
} else {
|
||||
infoDiv.style.display = 'none';
|
||||
dropBtn.disabled = true;
|
||||
truncateBtn.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Drop table
|
||||
document.getElementById('drop-table-btn')?.addEventListener('click', function() {
|
||||
const tableName = document.getElementById('table-to-drop').value;
|
||||
// Truncate table (clear data while preserving structure)
|
||||
document.getElementById('truncate-table-btn')?.addEventListener('click', function() {
|
||||
const tableName = document.getElementById('table-to-truncate').value;
|
||||
|
||||
if (!tableName) {
|
||||
showTableStatus('❌ Please select a table', 'error');
|
||||
@@ -2057,10 +2109,15 @@ document.getElementById('drop-table-btn')?.addEventListener('click', function()
|
||||
}
|
||||
|
||||
const tableData = tablesData.find(t => t.name === tableName);
|
||||
const confirmMessage = `⚠️ DANGER: Are you absolutely sure you want to DROP the table "${tableName}"?\n\n` +
|
||||
`This will permanently delete:\n` +
|
||||
`- ${tableData.rows} rows of data\n` +
|
||||
`- ${tableData.size} of storage\n\n` +
|
||||
const rowCount = tableData ? tableData.rows : '0';
|
||||
|
||||
const confirmMessage = `⚠️ WARNING: Clear table data?\n\n` +
|
||||
`You are about to clear all data from: "${tableName}"\n\n` +
|
||||
`Current rows: ${rowCount}\n\n` +
|
||||
`This will:\n` +
|
||||
`✓ DELETE all data\n` +
|
||||
`✓ PRESERVE table structure\n` +
|
||||
`✓ PRESERVE all triggers/functions\n\n` +
|
||||
`This action CANNOT be undone!\n\n` +
|
||||
`Type the table name to confirm: "${tableName}"`;
|
||||
|
||||
@@ -2074,9 +2131,9 @@ document.getElementById('drop-table-btn')?.addEventListener('click', function()
|
||||
const btn = this;
|
||||
const originalText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Dropping...';
|
||||
btn.textContent = '⏳ Clearing data...';
|
||||
|
||||
fetch('/api/maintenance/drop-table', {
|
||||
fetch('/api/maintenance/truncate-table', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -2091,9 +2148,10 @@ document.getElementById('drop-table-btn')?.addEventListener('click', function()
|
||||
btn.textContent = originalText;
|
||||
|
||||
if (data.success) {
|
||||
showTableStatus('✅ ' + data.message, 'success');
|
||||
const successMsg = `✅ ${data.message}\n\nRows cleared: ${data.rows_cleared}\nStructure preserved: ${data.structure_preserved ? 'Yes' : 'No'}`;
|
||||
showTableStatus(successMsg, 'success');
|
||||
// Reset and reload
|
||||
document.getElementById('table-to-drop').value = '';
|
||||
document.getElementById('table-to-truncate').value = '';
|
||||
document.getElementById('table-info').style.display = 'none';
|
||||
btn.disabled = true;
|
||||
// Reload tables list
|
||||
@@ -2106,10 +2164,10 @@ document.getElementById('drop-table-btn')?.addEventListener('click', function()
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error dropping table:', error);
|
||||
console.error('Error truncating table:', error);
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
showTableStatus('❌ Error dropping table', 'error');
|
||||
showTableStatus('❌ Error clearing table', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2179,6 +2237,15 @@ if (document.getElementById('log-retention-days')) {
|
||||
|
||||
// Backup now button
|
||||
document.getElementById('backup-now-btn')?.addEventListener('click', function() {
|
||||
const confirmed = confirm(
|
||||
'🗄️ CREATE FULL BACKUP?\n\n' +
|
||||
'⚠️ Warning: This will create a complete backup of the entire database (schema + data).\n\n' +
|
||||
'The operation may take a few moments depending on database size.\n\n' +
|
||||
'Are you sure you want to proceed?'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const btn = this;
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
@@ -2214,6 +2281,15 @@ document.getElementById('backup-now-btn')?.addEventListener('click', function()
|
||||
|
||||
// Data-only backup button
|
||||
document.getElementById('backup-data-only-btn')?.addEventListener('click', function() {
|
||||
const confirmed = confirm(
|
||||
'📦 CREATE DATA-ONLY BACKUP?\n\n' +
|
||||
'⚠️ Warning: This will create a backup of the database data only (no schema or triggers).\n\n' +
|
||||
'The operation may take a few moments depending on database size.\n\n' +
|
||||
'Are you sure you want to proceed?'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const btn = this;
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
@@ -2249,7 +2325,157 @@ document.getElementById('backup-data-only-btn')?.addEventListener('click', funct
|
||||
|
||||
// Refresh backups button
|
||||
document.getElementById('refresh-backups-btn')?.addEventListener('click', function() {
|
||||
loadBackupList();
|
||||
const btn = this;
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `
|
||||
<span class="btn-icon">⏳</span>
|
||||
<span class="btn-text">
|
||||
<strong>Refreshing...</strong>
|
||||
<small>Please wait</small>
|
||||
</span>
|
||||
`;
|
||||
|
||||
// Simulate a brief delay to show refreshing state
|
||||
setTimeout(() => {
|
||||
loadBackupList();
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHTML;
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Upload backup button
|
||||
// Upload backup file
|
||||
document.getElementById('upload-backup-btn')?.addEventListener('click', function() {
|
||||
document.getElementById('backup-file-input').click();
|
||||
});
|
||||
|
||||
document.getElementById('backup-file-input')?.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file extension
|
||||
if (!file.name.toLowerCase().endsWith('.sql') && !file.name.toLowerCase().endsWith('.gz')) {
|
||||
alert('❌ Invalid file format. Only .sql and .gz files are allowed.');
|
||||
e.target.value = '';
|
||||
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.');
|
||||
e.target.value = '';
|
||||
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?`)) {
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Final confirmation
|
||||
const confirmed = confirm(
|
||||
'📤 UPLOAD BACKUP FILE?\n\n' +
|
||||
'Filename: ' + file.name + '\n' +
|
||||
'Size: ' + (file.size / (1024 * 1024)).toFixed(2) + ' MB\n\n' +
|
||||
'⚠️ Warning: This file will be stored in the backup directory.\n' +
|
||||
'Make sure the file is a valid SQL backup file.\n\n' +
|
||||
'Are you sure you want to upload this file?'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
e.target.value = ''; // Clear input
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare form data
|
||||
const formData = new FormData();
|
||||
formData.append('backup_file', file);
|
||||
|
||||
const uploadBtn = document.getElementById('upload-backup-btn');
|
||||
const originalHTML = uploadBtn.innerHTML;
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '⏳ Uploading...';
|
||||
uploadBtn.title = 'Uploading...';
|
||||
|
||||
fetch('/api/backup/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalHTML;
|
||||
uploadBtn.title = 'Upload backup files';
|
||||
e.target.value = ''; // Clear input
|
||||
|
||||
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);
|
||||
loadBackupList(); // Refresh the list
|
||||
} 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);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error uploading backup:', error);
|
||||
alert('❌ Failed to upload backup file');
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalHTML;
|
||||
uploadBtn.title = 'Upload backup files';
|
||||
e.target.value = ''; // Clear input
|
||||
});
|
||||
});
|
||||
|
||||
// Add schedule button - show form
|
||||
@@ -2263,8 +2489,8 @@ document.getElementById('add-schedule-btn')?.addEventListener('click', function(
|
||||
document.getElementById('schedule-backup-type').value = 'full';
|
||||
document.getElementById('retention-days').value = '30';
|
||||
|
||||
// Hide schedules list, show form
|
||||
document.getElementById('schedules-list').style.display = 'none';
|
||||
// Hide hint and show form
|
||||
document.getElementById('schedule-form-hint').style.display = 'none';
|
||||
document.getElementById('backup-schedule-form').style.display = 'block';
|
||||
});
|
||||
|
||||
@@ -2285,13 +2511,29 @@ function editSchedule(scheduleId) {
|
||||
document.getElementById('schedule-backup-type').value = schedule.backup_type;
|
||||
document.getElementById('retention-days').value = schedule.retention_days;
|
||||
|
||||
// Hide schedules list, show form
|
||||
document.getElementById('schedules-list').style.display = 'none';
|
||||
document.getElementById('backup-schedule-form').style.display = 'block';
|
||||
console.log('✅ Schedule loaded:', schedule.id);
|
||||
|
||||
// Hide hint, show form
|
||||
const formElement = document.getElementById('backup-schedule-form');
|
||||
const hintElement = document.getElementById('schedule-form-hint');
|
||||
|
||||
if (formElement && hintElement) {
|
||||
formElement.style.display = 'block';
|
||||
hintElement.style.display = 'none';
|
||||
console.log('✅ Form displayed, hint hidden');
|
||||
} else {
|
||||
console.error('❌ Form or hint element not found');
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Schedule not found:', scheduleId);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Failed to load schedules');
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error loading schedule:', error));
|
||||
.catch(error => {
|
||||
console.error('❌ Error loading schedule:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle schedule function
|
||||
@@ -2345,7 +2587,7 @@ function deleteSchedule(scheduleId) {
|
||||
// Cancel schedule button - hide form
|
||||
document.getElementById('cancel-schedule-btn')?.addEventListener('click', function() {
|
||||
document.getElementById('backup-schedule-form').style.display = 'none';
|
||||
document.getElementById('schedules-list').style.display = 'block';
|
||||
document.getElementById('schedule-form-hint').style.display = 'block';
|
||||
});
|
||||
|
||||
// Save schedule form
|
||||
@@ -2590,122 +2832,36 @@ document.getElementById('restore-btn')?.addEventListener('click', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// 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`;
|
||||
// Load backup location path
|
||||
function loadBackupPath() {
|
||||
fetch('/api/backup/path')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const pathElement = document.getElementById('backup-location-path');
|
||||
if (pathElement) {
|
||||
if (data.success && data.backup_path) {
|
||||
pathElement.textContent = data.backup_path;
|
||||
} else {
|
||||
// Use fallback if API fails
|
||||
pathElement.textContent = '/srv/quality_app/backups';
|
||||
}
|
||||
}
|
||||
|
||||
// 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`;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading backup path:', error);
|
||||
// Set fallback if fetch fails
|
||||
const pathElement = document.getElementById('backup-location-path');
|
||||
if (pathElement) {
|
||||
pathElement.textContent = '/srv/quality_app/backups';
|
||||
}
|
||||
|
||||
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();
|
||||
loadBackupPath();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user