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:
Quality App Developer
2026-01-09 10:51:43 +02:00
parent 77463c1c47
commit 8faf5cd9fe
5 changed files with 363 additions and 2017 deletions

View File

@@ -1,13 +0,0 @@
{
"schedules": [
{
"id": "default",
"name": "Default Schedule",
"enabled": true,
"time": "03:00",
"frequency": "daily",
"backup_type": "data-only",
"retention_days": 30
}
]
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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="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="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: 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>