2712 lines
105 KiB
HTML
Executable File
2712 lines
105 KiB
HTML
Executable File
{% extends "base.html" %}
|
||
|
||
{% block title %}Settings{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="card-container">
|
||
<div class="card">
|
||
<h3>Manage Users (Legacy)</h3>
|
||
<ul class="user-list">
|
||
{% for user in users %}
|
||
<li data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">
|
||
<span class="user-name">{{ user.username }}</span>
|
||
<span class="user-role">Role: {{ user.role }}</span>
|
||
<button class="btn edit-user-btn" data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">Edit User</button>
|
||
<button class="btn delete-btn delete-user-btn" data-user-id="{{ user.id }}" data-username="{{ user.username }}">Delete User</button>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
<button id="create-user-btn" class="btn create-btn">Create User</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>External Server Settings</h3>
|
||
<form method="POST" action="{{ url_for('main.save_external_db') }}" class="form-centered">
|
||
<label for="db_server_domain">Server Domain/IP Address:</label>
|
||
<input type="text" id="db_server_domain" name="server_domain" value="{{ external_settings.get('server_domain', '') }}" required>
|
||
<label for="db_port">Port:</label>
|
||
<input type="number" id="db_port" name="port" value="{{ external_settings.get('port', '') }}" required>
|
||
<label for="db_database_name">Database Name:</label>
|
||
<input type="text" id="db_database_name" name="database_name" value="{{ external_settings.get('database_name', '') }}" required>
|
||
<label for="db_username">Username:</label>
|
||
<input type="text" id="db_username" name="username" value="{{ external_settings.get('username', '') }}" required>
|
||
<label for="db_password">Password:</label>
|
||
<input type="password" id="db_password" name="password" value="{{ external_settings.get('password', '') }}" required>
|
||
<button type="submit" class="btn">Save/Update External Database Info Settings</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top: 32px;">
|
||
<h3>🎯 User & Permissions Management</h3>
|
||
<p><strong>Simplified 4-Tier System:</strong> Superadmin → Admin → Manager → Worker</p>
|
||
<p>Streamlined interface with module-based permissions (Quality, Warehouse, Labels)</p>
|
||
<div style="margin-top: 15px;">
|
||
<a href="{{ url_for('main.user_management_simple') }}" class="btn" style="background-color: #2196f3; color: white; margin-right: 10px;">
|
||
🎯 Manage Users (Simplified)
|
||
</a>
|
||
|
||
</div>
|
||
<small style="display: block; margin-top: 10px; color: #666;">
|
||
Recommended: Use the simplified user management for easier administration
|
||
</small>
|
||
</div>
|
||
|
||
{% if session.role == 'superadmin' %}
|
||
<div class="card" style="margin-top: 32px;">
|
||
<h3>🖨️ Print Extension Management</h3>
|
||
<p><strong>QZ Tray Pairing Keys:</strong> Manage printer connection keys</p>
|
||
<p>Control access to direct printing functionality for label modules</p>
|
||
<div style="margin-top: 15px;">
|
||
<a href="{{ url_for('main.download_extension') }}" class="btn" style="background-color: #4caf50; color: white;">
|
||
🔑 Manage Pairing Keys
|
||
</a>
|
||
</div>
|
||
<small style="display: block; margin-top: 10px; color: #666;">
|
||
Generate and manage pairing keys for QZ Tray printer integration
|
||
</small>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if session.role in ['superadmin', 'admin'] %}
|
||
<div class="card" style="margin-top: 32px;">
|
||
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px;">
|
||
🔧 Maintenance & Cleanup
|
||
<span style="font-size: 0.7em; background: var(--accent-color, #4caf50); color: white; padding: 4px 12px; border-radius: 12px; font-weight: normal;">System</span>
|
||
</h3>
|
||
|
||
<!-- Log Files Auto-Delete 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;">
|
||
<h4 style="margin: 0 0 15px 0; color: var(--text-primary, #333);">📋 Log Files Auto-Delete</h4>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary, #666);">
|
||
Delete logs older than:
|
||
</label>
|
||
<select id="log-retention-days" style="padding: 10px 14px; border: 1px solid var(--input-border, #ddd); border-radius: 6px; font-size: 1em; min-width: 220px; background: var(--input-bg, white); color: var(--text-primary, #333);">
|
||
<option value="7">7 Days (1 Week)</option>
|
||
<option value="14">14 Days (2 Weeks)</option>
|
||
<option value="30" selected>30 Days (1 Month)</option>
|
||
<option value="60">60 Days (2 Months)</option>
|
||
<option value="90">90 Days (3 Months)</option>
|
||
<option value="180">180 Days (6 Months)</option>
|
||
<option value="365">365 Days (1 Year)</option>
|
||
<option value="0">Never (Disable Auto-Delete)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||
<button id="save-log-settings-btn" class="btn" style="background-color: #4caf50; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
|
||
💾 Save Settings
|
||
</button>
|
||
<button id="cleanup-logs-now-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;">
|
||
🗑️ Clean Up Logs Now
|
||
</button>
|
||
</div>
|
||
|
||
<div id="log-cleanup-status" style="margin-top: 15px; padding: 12px 16px; background: var(--status-bg, #e3f2fd); border-left: 4px solid var(--status-border, #2196f3); border-radius: 4px; display: none; color: var(--text-primary, #333);">
|
||
<strong>Status:</strong> <span id="log-cleanup-message"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- System Storage Information 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;">
|
||
<h4 style="margin: 0 0 15px 0; color: var(--text-primary, #333);">📊 System Storage Information</h4>
|
||
|
||
<div id="storage-info" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 15px;">
|
||
<div class="storage-stat-card" style="padding: 16px; background: linear-gradient(135deg, var(--stat-bg-1, rgba(33, 150, 243, 0.1)) 0%, var(--stat-bg-1-end, rgba(33, 150, 243, 0.05)) 100%); border: 1px solid var(--stat-border-1, rgba(33, 150, 243, 0.2)); border-radius: 8px; transition: transform 0.2s;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||
<span style="font-size: 1.5em;">📄</span>
|
||
<div style="font-size: 0.85em; color: var(--text-secondary, #666); font-weight: 500;">Log Files</div>
|
||
</div>
|
||
<div style="font-size: 1.4em; font-weight: 700; color: var(--stat-color-1, #1976d2);" id="logs-size">Loading...</div>
|
||
</div>
|
||
|
||
<div class="storage-stat-card" style="padding: 16px; background: linear-gradient(135deg, var(--stat-bg-2, rgba(156, 39, 176, 0.1)) 0%, var(--stat-bg-2-end, rgba(156, 39, 176, 0.05)) 100%); border: 1px solid var(--stat-border-2, rgba(156, 39, 176, 0.2)); border-radius: 8px; transition: transform 0.2s;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||
<span style="font-size: 1.5em;">💾</span>
|
||
<div style="font-size: 0.85em; color: var(--text-secondary, #666); font-weight: 500;">Database</div>
|
||
</div>
|
||
<div style="font-size: 1.4em; font-weight: 700; color: var(--stat-color-2, #7b1fa2);" id="database-size">Loading...</div>
|
||
</div>
|
||
|
||
<div class="storage-stat-card" style="padding: 16px; background: linear-gradient(135deg, var(--stat-bg-3, rgba(255, 152, 0, 0.1)) 0%, var(--stat-bg-3-end, rgba(255, 152, 0, 0.05)) 100%); border: 1px solid var(--stat-border-3, rgba(255, 152, 0, 0.2)); border-radius: 8px; transition: transform 0.2s;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||
<span style="font-size: 1.5em;">📦</span>
|
||
<div style="font-size: 0.85em; color: var(--text-secondary, #666); font-weight: 500;">Backups</div>
|
||
</div>
|
||
<div style="font-size: 1.4em; font-weight: 700; color: var(--stat-color-3, #f57c00);" id="backups-size">Loading...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button id="refresh-storage-btn" class="btn" style="background-color: var(--secondary-btn, #2196f3); color: white; font-size: 0.9em; padding: 8px 16px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
|
||
🔄 Refresh Storage Info
|
||
</button>
|
||
</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;">
|
||
<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>
|
||
</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="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>
|
||
</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!
|
||
</p>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<button id="load-tables-btn" class="btn" style="background-color: #2196f3; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
|
||
📋 Load Database Tables
|
||
</button>
|
||
</div>
|
||
|
||
<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:
|
||
</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);">
|
||
<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>
|
||
|
||
<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>
|
||
</div>
|
||
|
||
<div id="table-operation-status" style="margin-top: 15px; padding: 12px 16px; background: var(--status-bg, #e3f2fd); border-left: 4px solid var(--status-border, #2196f3); border-radius: 4px; display: none; color: var(--text-primary, #333);">
|
||
<strong>Status:</strong> <span id="table-operation-message"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if session.role in ['superadmin', 'admin'] %}
|
||
<div class="card backup-card" style="margin-top: 32px;">
|
||
<h3 style="margin: 0 0 20px 0; display: flex; align-items: center; gap: 10px; font-size: 1.3em;">
|
||
💾 Database Backup Management
|
||
<span style="font-size: 0.65em; background: #4caf50; color: white; padding: 3px 10px; border-radius: 12px; font-weight: normal;">Active</span>
|
||
</h3>
|
||
|
||
<!-- Quick Actions -->
|
||
<div style="display: flex; gap: 10px; margin-bottom: 24px; flex-wrap: wrap;">
|
||
<button id="backup-now-btn" class="compact-btn" style="background: #4caf50; color: white;">
|
||
🗄️ Full Backup
|
||
</button>
|
||
<button id="backup-data-only-btn" class="compact-btn" style="background: #2196f3; color: white;">
|
||
📦 Data Backup
|
||
</button>
|
||
<button id="refresh-backups-btn" class="compact-btn" style="background: #ff9800; color: white;">
|
||
🔄 Refresh
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Per-Table Operations Section -->
|
||
<div style="margin-bottom: 24px;">
|
||
<button id="toggle-table-backup-btn" class="compact-btn" style="width: 100%; background: #9c27b0; color: white; padding: 12px; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 1em;">
|
||
<span>📋</span>
|
||
<span id="toggle-table-backup-text">Show Per-Table Backup & Restore</span>
|
||
<span style="margin-left: auto; font-size: 1.2em;">▼</span>
|
||
</button>
|
||
|
||
<div id="table-backup-section" style="display: none; margin-top: 12px; padding: 20px; background: var(--sub-card-bg, rgba(156, 39, 176, 0.05)); border-radius: 8px; border: 1px solid var(--border-color, rgba(156, 39, 176, 0.2));">
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 16px;">
|
||
<!-- Backup Table Column -->
|
||
<div>
|
||
<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;">
|
||
<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);">
|
||
<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>
|
||
💾 Create Backup
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restore Table Column -->
|
||
<div>
|
||
<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;">
|
||
<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);">
|
||
<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>
|
||
🔄 Restore Table
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="table-backup-status" style="padding: 12px 16px; background: var(--status-bg, #e3f2fd); border-left: 4px solid var(--status-border, #2196f3); border-radius: 4px; display: none; color: var(--text-primary, #333);">
|
||
<strong>Status:</strong> <span id="table-backup-message"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Grid Layout for Cards -->
|
||
<div class="backup-grid" style="display: grid; grid-template-columns: 1fr 2fr; gap: 16px;">
|
||
|
||
<!-- Create Backup Card -->
|
||
<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);">
|
||
<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;">
|
||
<!-- Add/Edit Schedule Form -->
|
||
<form id="backup-schedule-form" class="schedule-compact-form" style="font-size: 0.9em;">
|
||
<input type="hidden" id="schedule-id" name="id">
|
||
|
||
<div style="margin-bottom: 10px;">
|
||
<label style="display: block; margin-bottom: 4px; font-weight: 600; font-size: 0.85em;">Name</label>
|
||
<input type="text" id="schedule-name" name="name" placeholder="e.g., Monthly Backup" required style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px;">
|
||
<div>
|
||
<label style="display: block; margin-bottom: 4px; font-weight: 600; font-size: 0.85em;">Time</label>
|
||
<input type="time" id="schedule-time" name="time" value="02:00" required style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||
</div>
|
||
<div>
|
||
<label style="display: block; margin-bottom: 4px; font-weight: 600; font-size: 0.85em;">Frequency</label>
|
||
<select id="schedule-frequency" name="frequency" required style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||
<option value="daily">Daily</option>
|
||
<option value="weekly">Weekly</option>
|
||
<option value="monthly">Monthly</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px;">
|
||
<div>
|
||
<label style="display: block; margin-bottom: 4px; font-weight: 600; font-size: 0.85em;">Type</label>
|
||
<select id="schedule-backup-type" name="backup_type" required style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||
<option value="full">Full</option>
|
||
<option value="data-only">Data-Only</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display: block; margin-bottom: 4px; font-weight: 600; font-size: 0.85em;">Keep (days)</label>
|
||
<input type="number" id="retention-days" name="retention_days" value="30" min="1" max="365" required style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px;">
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 10px;">
|
||
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
|
||
<input type="checkbox" id="schedule-enabled" name="enabled" checked>
|
||
<span style="font-size: 0.85em;">Enable this schedule</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 8px;">
|
||
<button type="submit" style="flex: 1; padding: 6px; background: #4caf50; color: white; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; font-size: 0.85em;">💾 Save</button>
|
||
<button type="button" id="cancel-schedule-btn" style="flex: 1; padding: 6px; background: #999; color: white; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; font-size: 0.85em;">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Schedules List Card -->
|
||
<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);">⏰ Active Schedules</h4>
|
||
<div style="display: flex; gap: 6px; align-items: center;">
|
||
<span id="schedule-count-badge" style="background: #2196f3; color: white; font-size: 0.75em; padding: 3px 8px; border-radius: 10px; font-weight: 600;">0</span>
|
||
<button id="add-schedule-btn" class="btn-small" style="background: #4caf50; color: white; padding: 4px 10px; border-radius: 4px; font-size: 0.8em; border: none; cursor: pointer;">
|
||
➕
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="sub-card-body" style="padding: 12px; max-height: 300px; overflow-y: auto;">
|
||
<div id="schedules-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Available Backups -->
|
||
<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>
|
||
<div class="sub-card-body" style="padding: 12px; max-height: 300px; overflow-y: auto;">
|
||
<div id="backup-list" class="backup-list-modern">
|
||
<div style="text-align: center; padding: 20px; color: #999; font-size: 0.9em;">
|
||
<div style="font-size: 2em; margin-bottom: 8px;">⏳</div>
|
||
<p style="margin: 0;">Loading...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- 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>
|
||
|
||
<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>
|
||
</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>
|
||
/* Compact Button Styles */
|
||
.compact-btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
font-size: 0.9em;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.compact-btn:hover:not(:disabled) {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.compact-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Modern Backup Card Styles */
|
||
.backup-card {
|
||
background: var(--card-bg, #fff);
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.backup-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.backup-sub-card {
|
||
background: var(--sub-card-bg, #fafafa);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--sub-card-border, #e0e0e0);
|
||
}
|
||
|
||
.sub-card-header {
|
||
background: var(--sub-header-bg, #f5f5f5);
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--sub-card-border, #e0e0e0);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.sub-card-header h4 {
|
||
margin: 0;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
color: var(--text-color, #333);
|
||
}
|
||
|
||
.sub-card-body {
|
||
padding: 16px;
|
||
}
|
||
|
||
/* Status Badges */
|
||
.status-badge {
|
||
font-size: 0.75em;
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status-enabled {
|
||
background: #4caf50;
|
||
color: white;
|
||
}
|
||
|
||
.status-disabled {
|
||
background: #9e9e9e;
|
||
color: white;
|
||
}
|
||
|
||
.count-badge {
|
||
background: #2196f3;
|
||
color: white;
|
||
font-size: 0.85em;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.warning-badge {
|
||
background: #ff5722;
|
||
color: white;
|
||
font-size: 0.75em;
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Quick Action Buttons */
|
||
.backup-action-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
margin-bottom: 10px;
|
||
border: 2px solid;
|
||
border-radius: 8px;
|
||
background: var(--btn-bg, #fff);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.backup-action-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.backup-action-btn .btn-icon {
|
||
font-size: 1.8em;
|
||
line-height: 1;
|
||
}
|
||
|
||
.backup-action-btn .btn-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
text-align: left;
|
||
}
|
||
|
||
.backup-action-btn .btn-text strong {
|
||
font-size: 0.95em;
|
||
color: var(--text-color, #333);
|
||
}
|
||
|
||
.backup-action-btn .btn-text small {
|
||
font-size: 0.8em;
|
||
color: var(--text-secondary, #666);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.backup-btn-full {
|
||
border-color: #4caf50;
|
||
}
|
||
|
||
.backup-btn-full:hover {
|
||
background: #e8f5e9;
|
||
border-color: #388e3c;
|
||
}
|
||
|
||
.backup-btn-data {
|
||
border-color: #ff9800;
|
||
}
|
||
|
||
.backup-btn-data:hover {
|
||
background: #fff3e0;
|
||
border-color: #f57c00;
|
||
}
|
||
|
||
.backup-btn-refresh {
|
||
border-color: #2196f3;
|
||
}
|
||
|
||
.backup-btn-refresh:hover {
|
||
background: #e3f2fd;
|
||
border-color: #1976d2;
|
||
}
|
||
|
||
/* Compact Schedule Form */
|
||
.schedule-compact-form {
|
||
animation: slideDown 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.form-row {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.form-row-split {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
font-size: 0.85em;
|
||
font-weight: 600;
|
||
color: var(--label-color, #555);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--input-border, #ddd);
|
||
border-radius: 6px;
|
||
background: var(--input-bg, #fff);
|
||
color: var(--text-color, #333);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
outline: none;
|
||
border-color: #2196f3;
|
||
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
|
||
}
|
||
|
||
.checkbox-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 600;
|
||
color: var(--text-color, #333);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.checkbox-label input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.btn-save {
|
||
flex: 1;
|
||
padding: 10px 20px;
|
||
background: #ff9800;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn-save:hover {
|
||
background: #f57c00;
|
||
}
|
||
|
||
.btn-cancel {
|
||
padding: 10px 20px;
|
||
background: var(--btn-cancel-bg, #e0e0e0);
|
||
color: var(--text-color, #333);
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn-cancel:hover {
|
||
background: var(--btn-cancel-hover, #bdbdbd);
|
||
}
|
||
|
||
.btn-small {
|
||
border: none;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-small:hover {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.setup-btn {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: #2196f3;
|
||
color: white;
|
||
border: 2px dashed #1976d2;
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.setup-btn:hover {
|
||
background: #1976d2;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
/* Schedule Items */
|
||
.schedule-item {
|
||
padding: 12px 16px;
|
||
margin-bottom: 10px;
|
||
background: var(--schedule-item-bg, #fff);
|
||
border: 2px solid var(--schedule-item-border, #e0e0e0);
|
||
border-radius: 8px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.schedule-item:hover {
|
||
border-color: #2196f3;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.schedule-item.disabled {
|
||
opacity: 0.6;
|
||
background: var(--schedule-disabled-bg, #fafafa);
|
||
}
|
||
|
||
.schedule-item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.schedule-item-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 600;
|
||
color: var(--text-color, #333);
|
||
}
|
||
|
||
.schedule-item-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.schedule-item-body {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 8px;
|
||
font-size: 0.85em;
|
||
color: var(--text-secondary, #666);
|
||
}
|
||
|
||
.schedule-detail {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.schedule-next-run {
|
||
margin-top: 8px;
|
||
padding: 8px 12px;
|
||
background: var(--next-run-bg, #e8f5e9);
|
||
border-left: 3px solid #4caf50;
|
||
border-radius: 4px;
|
||
font-size: 0.85em;
|
||
}
|
||
|
||
.schedule-next-run strong {
|
||
color: var(--next-run-text, #2e7d32);
|
||
}
|
||
|
||
.schedule-next-run-time {
|
||
font-weight: 600;
|
||
color: var(--next-run-time, #1b5e20);
|
||
}
|
||
|
||
.btn-icon-small {
|
||
padding: 4px 8px;
|
||
background: var(--btn-icon-bg, #f5f5f5);
|
||
border: 1px solid var(--btn-icon-border, #ddd);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.9em;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-icon-small:hover {
|
||
background: var(--btn-icon-hover, #e0e0e0);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.btn-icon-small.toggle-on {
|
||
background: #4caf50;
|
||
color: white;
|
||
border-color: #4caf50;
|
||
}
|
||
|
||
.btn-icon-small.toggle-off {
|
||
background: #9e9e9e;
|
||
color: white;
|
||
border-color: #9e9e9e;
|
||
}
|
||
|
||
.btn-icon-small.edit {
|
||
background: #2196f3;
|
||
color: white;
|
||
border-color: #2196f3;
|
||
}
|
||
|
||
.btn-icon-small.delete {
|
||
background: #f44336;
|
||
color: white;
|
||
border-color: #f44336;
|
||
}
|
||
|
||
.schedule-empty {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: var(--text-secondary, #999);
|
||
}
|
||
|
||
.schedule-empty .empty-icon {
|
||
font-size: 3em;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* Backup List Modern */
|
||
.backup-list-modern {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
background: var(--list-bg, #fff);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.backup-loading {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: var(--text-secondary, #999);
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid var(--spinner-bg, #f3f3f3);
|
||
border-top: 4px solid #2196f3;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 12px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.backup-item {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--item-border, #f0f0f0);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.backup-item:hover {
|
||
background: var(--item-hover, #f5f5f5);
|
||
}
|
||
|
||
.backup-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.backup-info-row {
|
||
flex: 1;
|
||
}
|
||
|
||
.backup-filename {
|
||
font-weight: 600;
|
||
color: var(--text-color, #333);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.backup-meta {
|
||
font-size: 0.85em;
|
||
color: var(--text-secondary, #666);
|
||
}
|
||
|
||
.backup-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.backup-action-icon {
|
||
padding: 6px 12px;
|
||
background: var(--action-bg, #f5f5f5);
|
||
border: 1px solid var(--action-border, #ddd);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.9em;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.backup-action-icon:hover {
|
||
background: var(--action-hover, #e0e0e0);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
/* Restore Card */
|
||
.restore-card {
|
||
border: 2px solid #ff9800;
|
||
}
|
||
|
||
.restore-card .sub-card-header {
|
||
background: #fff3e0;
|
||
}
|
||
|
||
.warning-message {
|
||
padding: 12px;
|
||
background: #fff3e0;
|
||
border-left: 4px solid #ff9800;
|
||
border-radius: 4px;
|
||
color: #e65100;
|
||
font-size: 0.9em;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.upload-section {
|
||
padding: 16px;
|
||
background: var(--upload-bg, #e3f2fd);
|
||
border-radius: 6px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.upload-label {
|
||
display: block;
|
||
font-weight: 600;
|
||
color: var(--text-color, #333);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.upload-row {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.file-input {
|
||
flex: 1;
|
||
padding: 8px;
|
||
border: 1px solid var(--input-border, #ddd);
|
||
border-radius: 6px;
|
||
background: var(--input-bg, #fff);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.btn-upload {
|
||
padding: 8px 20px;
|
||
background: #2196f3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn-upload:hover {
|
||
background: #1976d2;
|
||
}
|
||
|
||
.restore-section-compact {
|
||
padding: 16px;
|
||
background: var(--restore-bg, #f9f9f9);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.restore-select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid var(--input-border, #ddd);
|
||
border-radius: 6px;
|
||
background: var(--input-bg, #fff);
|
||
color: var(--text-color, #333);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.restore-type-options {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.radio-card {
|
||
position: relative;
|
||
display: block;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.radio-card input[type="radio"] {
|
||
position: absolute;
|
||
opacity: 0;
|
||
}
|
||
|
||
.radio-content {
|
||
padding: 12px;
|
||
border: 2px solid var(--radio-border, #ddd);
|
||
border-radius: 6px;
|
||
background: var(--radio-bg, #fff);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.radio-card input[type="radio"]:checked + .radio-content {
|
||
border-color: #2196f3;
|
||
background: #e3f2fd;
|
||
}
|
||
|
||
.radio-content strong {
|
||
display: block;
|
||
font-size: 0.9em;
|
||
color: var(--text-color, #333);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.radio-content small {
|
||
display: block;
|
||
font-size: 0.8em;
|
||
color: var(--text-secondary, #666);
|
||
}
|
||
|
||
.btn-restore {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: #ff5722;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
font-size: 1em;
|
||
cursor: pointer;
|
||
margin-top: 16px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-restore:disabled {
|
||
background: var(--btn-disabled, #bdbdbd);
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.btn-restore:not(:disabled):hover {
|
||
background: #e64a19;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(255, 87, 34, 0.3);
|
||
}
|
||
|
||
.backup-info-compact {
|
||
margin-top: 20px;
|
||
padding: 12px 16px;
|
||
background: var(--info-bg, #e3f2fd);
|
||
border-left: 4px solid #2196f3;
|
||
border-radius: 6px;
|
||
font-size: 0.9em;
|
||
color: var(--info-text, #0d47a1);
|
||
}
|
||
|
||
.backup-info-compact code {
|
||
background: var(--code-bg, #bbdefb);
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
color: var(--code-text, #01579b);
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.backup-info-compact small {
|
||
display: block;
|
||
margin-top: 4px;
|
||
color: var(--info-small, #1565c0);
|
||
}
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 768px) {
|
||
.backup-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.form-row-split {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.restore-type-options {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Dark Mode Styles */
|
||
body.dark-mode .backup-card {
|
||
background: #2d2d2d;
|
||
color: #e0e0e0;
|
||
--card-bg: #2d2d2d;
|
||
--text-color: #e0e0e0;
|
||
--text-secondary: #aaa;
|
||
}
|
||
|
||
body.dark-mode .backup-sub-card {
|
||
background: #3a3a3a;
|
||
border-color: #555;
|
||
--sub-card-bg: #3a3a3a;
|
||
--sub-card-border: #555;
|
||
}
|
||
|
||
body.dark-mode .sub-card-header {
|
||
background: #444;
|
||
border-bottom-color: #555;
|
||
--sub-header-bg: #444;
|
||
}
|
||
|
||
body.dark-mode .backup-action-btn {
|
||
background: #3a3a3a;
|
||
--btn-bg: #3a3a3a;
|
||
}
|
||
|
||
body.dark-mode .backup-action-btn .btn-text strong {
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
body.dark-mode .backup-action-btn .btn-text small {
|
||
color: #aaa;
|
||
}
|
||
|
||
body.dark-mode .backup-btn-full:hover {
|
||
background: #1b5e20;
|
||
}
|
||
|
||
body.dark-mode .backup-btn-data:hover {
|
||
background: #e65100;
|
||
}
|
||
|
||
body.dark-mode .backup-btn-refresh:hover {
|
||
background: #0d47a1;
|
||
}
|
||
|
||
body.dark-mode .form-group input,
|
||
body.dark-mode .form-group select,
|
||
body.dark-mode .restore-select,
|
||
body.dark-mode .file-input {
|
||
background: #2d2d2d;
|
||
border-color: #555;
|
||
color: #e0e0e0;
|
||
--input-bg: #2d2d2d;
|
||
--input-border: #555;
|
||
}
|
||
|
||
body.dark-mode .form-group label,
|
||
body.dark-mode .upload-label {
|
||
color: #bbb;
|
||
--label-color: #bbb;
|
||
}
|
||
|
||
body.dark-mode .btn-cancel {
|
||
background: #555;
|
||
color: #e0e0e0;
|
||
--btn-cancel-bg: #555;
|
||
}
|
||
|
||
body.dark-mode .btn-cancel:hover {
|
||
background: #666;
|
||
--btn-cancel-hover: #666;
|
||
}
|
||
|
||
body.dark-mode .backup-list-modern {
|
||
background: #2d2d2d;
|
||
--list-bg: #2d2d2d;
|
||
}
|
||
|
||
body.dark-mode .backup-item {
|
||
border-bottom-color: #444;
|
||
--item-border: #444;
|
||
}
|
||
|
||
body.dark-mode .backup-item:hover {
|
||
background: #3a3a3a;
|
||
--item-hover: #3a3a3a;
|
||
}
|
||
|
||
body.dark-mode .backup-action-icon {
|
||
background: #444;
|
||
border-color: #555;
|
||
color: #e0e0e0;
|
||
--action-bg: #444;
|
||
--action-border: #555;
|
||
}
|
||
|
||
body.dark-mode .backup-action-icon:hover {
|
||
background: #555;
|
||
--action-hover: #555;
|
||
}
|
||
|
||
body.dark-mode .upload-section {
|
||
background: #1e3a5f;
|
||
--upload-bg: #1e3a5f;
|
||
}
|
||
|
||
body.dark-mode .restore-section-compact {
|
||
background: #3a3a3a;
|
||
--restore-bg: #3a3a3a;
|
||
}
|
||
|
||
body.dark-mode .radio-content {
|
||
background: #2d2d2d;
|
||
border-color: #555;
|
||
--radio-bg: #2d2d2d;
|
||
--radio-border: #555;
|
||
}
|
||
|
||
body.dark-mode .radio-card input[type="radio"]:checked + .radio-content {
|
||
background: #0d47a1;
|
||
border-color: #2196f3;
|
||
}
|
||
|
||
body.dark-mode .backup-info-compact {
|
||
background: #1e3a5f;
|
||
color: #90caf9;
|
||
--info-bg: #1e3a5f;
|
||
--info-text: #90caf9;
|
||
}
|
||
|
||
body.dark-mode .backup-info-compact code {
|
||
background: #2d4a6d;
|
||
color: #64b5f6;
|
||
--code-bg: #2d4a6d;
|
||
--code-text: #64b5f6;
|
||
}
|
||
|
||
body.dark-mode .backup-info-compact small {
|
||
color: #81c784;
|
||
--info-small: #81c784;
|
||
}
|
||
|
||
body.dark-mode #current-schedule-display {
|
||
background: #1b5e20;
|
||
--schedule-info-bg: #1b5e20;
|
||
}
|
||
|
||
body.dark-mode #current-schedule-display strong {
|
||
color: #a5d6a7;
|
||
--schedule-text: #a5d6a7;
|
||
}
|
||
|
||
body.dark-mode #next-run-display {
|
||
color: #c8e6c9;
|
||
--schedule-time: #c8e6c9;
|
||
}
|
||
|
||
body.dark-mode #current-schedule-display > div:last-child {
|
||
color: #bbb;
|
||
--schedule-details: #bbb;
|
||
}
|
||
|
||
body.dark-mode .loading-spinner {
|
||
border-color: #444;
|
||
border-top-color: #2196f3;
|
||
--spinner-bg: #444;
|
||
}
|
||
|
||
body.dark-mode .restore-card {
|
||
border-color: #ff9800;
|
||
}
|
||
|
||
body.dark-mode .restore-card .sub-card-header {
|
||
background: #3a2a1f;
|
||
}
|
||
|
||
body.dark-mode .warning-message {
|
||
background: #3a2a1f;
|
||
color: #ffb74d;
|
||
}
|
||
|
||
body.dark-mode .schedule-item {
|
||
background: #3a3a3a;
|
||
border-color: #555;
|
||
--schedule-item-bg: #3a3a3a;
|
||
--schedule-item-border: #555;
|
||
}
|
||
|
||
body.dark-mode .schedule-item.disabled {
|
||
background: #2d2d2d;
|
||
--schedule-disabled-bg: #2d2d2d;
|
||
}
|
||
|
||
body.dark-mode .schedule-next-run {
|
||
background: #1b5e20;
|
||
--next-run-bg: #1b5e20;
|
||
}
|
||
|
||
body.dark-mode .schedule-next-run strong {
|
||
color: #a5d6a7;
|
||
--next-run-text: #a5d6a7;
|
||
}
|
||
|
||
body.dark-mode .schedule-next-run-time {
|
||
color: #c8e6c9;
|
||
--next-run-time: #c8e6c9;
|
||
}
|
||
|
||
body.dark-mode .btn-icon-small {
|
||
background: #444;
|
||
border-color: #555;
|
||
color: #e0e0e0;
|
||
--btn-icon-bg: #444;
|
||
--btn-icon-border: #555;
|
||
}
|
||
|
||
body.dark-mode .btn-icon-small:hover {
|
||
background: #555;
|
||
--btn-icon-hover: #555;
|
||
}
|
||
|
||
/* Maintenance Card - Storage Stats Hover Effect */
|
||
.storage-stat-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
/* Maintenance Card - Button Hover Effects */
|
||
#save-log-settings-btn:hover {
|
||
background-color: #45a049 !important;
|
||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
|
||
}
|
||
|
||
#cleanup-logs-now-btn:hover {
|
||
background-color: #fb8c00 !important;
|
||
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4);
|
||
}
|
||
|
||
#refresh-storage-btn:hover {
|
||
background-color: #1976d2 !important;
|
||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
|
||
}
|
||
|
||
/* Dark Mode Support for Maintenance Card */
|
||
body.dark-mode {
|
||
--sub-card-bg: rgba(255,255,255,0.05);
|
||
--border-color: rgba(255,255,255,0.1);
|
||
--input-border: rgba(255,255,255,0.2);
|
||
--input-bg: rgba(255,255,255,0.05);
|
||
--text-primary: #e0e0e0;
|
||
--text-secondary: #b0b0b0;
|
||
--code-bg: rgba(255,255,255,0.1);
|
||
|
||
/* Storage stat cards - dark mode */
|
||
--stat-bg-1: rgba(33, 150, 243, 0.15);
|
||
--stat-bg-1-end: rgba(33, 150, 243, 0.08);
|
||
--stat-border-1: rgba(33, 150, 243, 0.3);
|
||
--stat-color-1: #64b5f6;
|
||
|
||
--stat-bg-2: rgba(156, 39, 176, 0.15);
|
||
--stat-bg-2-end: rgba(156, 39, 176, 0.08);
|
||
--stat-border-2: rgba(156, 39, 176, 0.3);
|
||
--stat-color-2: #ba68c8;
|
||
|
||
--stat-bg-3: rgba(255, 152, 0, 0.15);
|
||
--stat-bg-3-end: rgba(255, 152, 0, 0.08);
|
||
--stat-border-3: rgba(255, 152, 0, 0.3);
|
||
--stat-color-3: #ffb74d;
|
||
|
||
--info-bg: rgba(76, 175, 80, 0.15);
|
||
--info-border: #66bb6a;
|
||
|
||
--status-bg: rgba(33, 150, 243, 0.15);
|
||
--status-border: #64b5f6;
|
||
|
||
--secondary-btn: #1976d2;
|
||
|
||
/* Table management dark mode */
|
||
--warning-bg: rgba(255, 87, 34, 0.15);
|
||
--warning-text: #ff7043;
|
||
--info-bg-alt: rgba(33, 150, 243, 0.15);
|
||
|
||
--status-success-bg: rgba(76, 175, 80, 0.15);
|
||
--status-success-border: #66bb6a;
|
||
--status-error-bg: rgba(244, 67, 54, 0.15);
|
||
--status-error-border: #ef5350;
|
||
}
|
||
|
||
/* Select dropdown dark mode */
|
||
body.dark-mode #log-retention-days,
|
||
body.dark-mode #table-to-drop {
|
||
background: rgba(255,255,255,0.05);
|
||
color: #e0e0e0;
|
||
border-color: rgba(255,255,255,0.2);
|
||
}
|
||
|
||
body.dark-mode #log-retention-days option,
|
||
body.dark-mode #table-to-drop option {
|
||
background: #2a2a2a;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
/* Button hover effects - dark mode */
|
||
body.dark-mode #load-tables-btn:hover {
|
||
background-color: #1976d2 !important;
|
||
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 #drop-table-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
</style>
|
||
|
||
<!-- Popup for creating/editing a user -->
|
||
<div id="user-popup" class="popup" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; background:var(--app-overlay-bg, rgba(30,41,59,0.85)); z-index:9999; align-items:center; justify-content:center;">
|
||
<div class="popup-content" style="margin:auto; padding:32px; border-radius:8px; box-shadow:0 2px 8px #333; min-width:320px; max-width:400px; text-align:center;">
|
||
<h3 id="user-popup-title">Create/Edit User</h3>
|
||
<form id="user-form" method="POST" action="{{ url_for('main.create_user') }}">
|
||
<input type="hidden" id="user-id" name="user_id">
|
||
<label for="user_username">Username:</label>
|
||
<input type="text" id="user_username" name="username" required>
|
||
<label for="user_email">Email (Optional):</label>
|
||
<input type="email" id="user_email" name="email">
|
||
<label for="user_password">Password:</label>
|
||
<input type="password" id="user_password" name="password" required>
|
||
<label for="user_role">Role:</label>
|
||
<select id="user_role" name="role" required>
|
||
<option value="superadmin">Superadmin</option>
|
||
<option value="admin">Admin</option>
|
||
<option value="manager">Manager</option>
|
||
<option value="warehouse_manager">Warehouse Manager</option>
|
||
<option value="warehouse_worker">Warehouse Worker</option>
|
||
<option value="quality_manager">Quality Manager</option>
|
||
<option value="quality_worker">Quality Worker</option>
|
||
</select>
|
||
<button type="submit" class="btn">Save</button>
|
||
<button type="button" id="close-user-popup-btn" class="btn cancel-btn">Cancel</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Popup for confirming user deletion -->
|
||
<div id="delete-user-popup" class="popup">
|
||
<div class="popup-content">
|
||
<h3>Do you really want to delete the user <span id="delete-username"></span>?</h3>
|
||
<form id="delete-user-form" method="POST" action="{{ url_for('main.delete_user') }}">
|
||
<input type="hidden" id="delete-user-id" name="user_id">
|
||
<button type="submit" class="btn delete-confirm-btn">Yes</button>
|
||
<button type="button" id="close-delete-popup-btn" class="btn cancel-btn">No</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
document.getElementById('create-user-btn').onclick = function() {
|
||
document.getElementById('user-popup').style.display = 'flex';
|
||
document.getElementById('user-popup-title').innerText = 'Create User';
|
||
document.getElementById('user-form').reset();
|
||
document.getElementById('user-form').setAttribute('action', '{{ url_for("main.create_user") }}');
|
||
document.getElementById('user-id').value = '';
|
||
document.getElementById('user_password').required = true;
|
||
document.getElementById('user_password').placeholder = '';
|
||
document.getElementById('user_username').readOnly = false;
|
||
};
|
||
|
||
document.getElementById('close-user-popup-btn').onclick = function() {
|
||
document.getElementById('user-popup').style.display = 'none';
|
||
};
|
||
|
||
// Edit User button logic
|
||
Array.from(document.getElementsByClassName('edit-user-btn')).forEach(function(btn) {
|
||
btn.onclick = function() {
|
||
document.getElementById('user-popup').style.display = 'flex';
|
||
document.getElementById('user-popup-title').innerText = 'Edit User';
|
||
document.getElementById('user-id').value = btn.getAttribute('data-user-id');
|
||
document.getElementById('user_username').value = btn.getAttribute('data-username');
|
||
document.getElementById('user_email').value = btn.getAttribute('data-email') || '';
|
||
document.getElementById('user_role').value = btn.getAttribute('data-role');
|
||
document.getElementById('user_password').value = '';
|
||
document.getElementById('user_password').required = false;
|
||
document.getElementById('user_password').placeholder = 'Leave blank to keep current password';
|
||
document.getElementById('user_username').readOnly = true;
|
||
document.getElementById('user-form').setAttribute('action', '{{ url_for("main.edit_user") }}');
|
||
};
|
||
});
|
||
|
||
// Delete User button logic
|
||
Array.from(document.getElementsByClassName('delete-user-btn')).forEach(function(btn) {
|
||
btn.onclick = function() {
|
||
document.getElementById('delete-user-popup').style.display = 'flex';
|
||
document.getElementById('delete-username').innerText = btn.getAttribute('data-username');
|
||
document.getElementById('delete-user-id').value = btn.getAttribute('data-user-id');
|
||
};
|
||
});
|
||
|
||
document.getElementById('close-delete-popup-btn').onclick = function() {
|
||
document.getElementById('delete-user-popup').style.display = 'none';
|
||
};
|
||
|
||
// ========================================
|
||
// Database Backup Management Functions
|
||
// ========================================
|
||
|
||
// Load backup schedules on page load
|
||
function loadBackupSchedule() {
|
||
fetch('/api/backup/schedule')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const scheduleConfig = data.schedule;
|
||
const jobs = data.jobs || [];
|
||
const schedules = scheduleConfig.schedules || [];
|
||
|
||
// Update count badge
|
||
const enabledCount = schedules.filter(s => s.enabled).length;
|
||
document.getElementById('schedule-count-badge').textContent = enabledCount;
|
||
|
||
// Render schedules list
|
||
const schedulesList = document.getElementById('schedules-list');
|
||
|
||
if (schedules.length === 0) {
|
||
schedulesList.innerHTML = `
|
||
<div class="schedule-empty">
|
||
<div class="empty-icon">⏰</div>
|
||
<p style="margin: 0; font-weight: 600;">No schedules configured</p>
|
||
<small>Click "Add Schedule" to create automatic backups</small>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
schedules.forEach(schedule => {
|
||
const job = jobs.find(j => j.id === schedule.id);
|
||
const nextRun = job ? job.next_run_time : null;
|
||
const isEnabled = schedule.enabled;
|
||
const disabledClass = isEnabled ? '' : 'disabled';
|
||
|
||
// Format frequency
|
||
const freqMap = {
|
||
'daily': 'Daily',
|
||
'weekly': 'Weekly (Sun)',
|
||
'monthly': 'Monthly (1st)'
|
||
};
|
||
const freqDisplay = freqMap[schedule.frequency] || schedule.frequency;
|
||
|
||
// Format backup type
|
||
const typeDisplay = schedule.backup_type === 'data-only' ? '📦 Data-Only' : '🗄️ Full';
|
||
|
||
html += `
|
||
<div class="schedule-item ${disabledClass}" data-schedule-id="${schedule.id}">
|
||
<div class="schedule-item-header">
|
||
<div class="schedule-item-title">
|
||
${isEnabled ? '✅' : '⏸️'}
|
||
<span>${schedule.name || schedule.id}</span>
|
||
</div>
|
||
<div class="schedule-item-actions">
|
||
<button class="btn-icon-small ${isEnabled ? 'toggle-on' : 'toggle-off'}"
|
||
onclick="toggleSchedule('${schedule.id}')"
|
||
title="${isEnabled ? 'Disable' : 'Enable'}">
|
||
${isEnabled ? '🔵' : '⏸️'}
|
||
</button>
|
||
<button class="btn-icon-small edit"
|
||
onclick="editSchedule('${schedule.id}')"
|
||
title="Edit">
|
||
✏️
|
||
</button>
|
||
${schedule.id !== 'default' ? `
|
||
<button class="btn-icon-small delete"
|
||
onclick="deleteSchedule('${schedule.id}')"
|
||
title="Delete">
|
||
🗑️
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="schedule-item-body">
|
||
<div class="schedule-detail">
|
||
<strong>⏰</strong> ${schedule.time}
|
||
</div>
|
||
<div class="schedule-detail">
|
||
<strong>📅</strong> ${freqDisplay}
|
||
</div>
|
||
<div class="schedule-detail">
|
||
<strong>💾</strong> ${typeDisplay}
|
||
</div>
|
||
<div class="schedule-detail">
|
||
<strong>🗓️</strong> Keep ${schedule.retention_days}d
|
||
</div>
|
||
</div>
|
||
${nextRun && isEnabled ? `
|
||
<div class="schedule-next-run">
|
||
<strong>Next Backup:</strong>
|
||
<span class="schedule-next-run-time">${nextRun}</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
schedulesList.innerHTML = html;
|
||
}
|
||
})
|
||
.catch(error => console.error('Error loading schedules:', error));
|
||
}
|
||
|
||
// Load backup list
|
||
function loadBackupList() {
|
||
const backupList = document.getElementById('backup-list');
|
||
if (!backupList) return;
|
||
|
||
backupList.innerHTML = `
|
||
<div class="backup-loading">
|
||
<div class="loading-spinner"></div>
|
||
<p>Loading backups...</p>
|
||
</div>
|
||
`;
|
||
|
||
fetch('/api/backup/list')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && data.backups.length > 0) {
|
||
// Update count badge
|
||
document.getElementById('backup-count-badge').textContent = data.backups.length;
|
||
|
||
let html = '';
|
||
data.backups.forEach(backup => {
|
||
html += `
|
||
<div class="backup-item">
|
||
<div class="backup-info-row">
|
||
<div class="backup-filename">📄 ${backup.filename}</div>
|
||
<div class="backup-meta">
|
||
💾 ${backup.size_mb} MB • 📅 ${backup.created}
|
||
</div>
|
||
</div>
|
||
<div class="backup-actions">
|
||
<button onclick="downloadBackup('${backup.filename}')" class="backup-action-icon" title="Download">
|
||
⬇️
|
||
</button>
|
||
<button onclick="deleteBackup('${backup.filename}')" class="backup-action-icon" title="Delete">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
backupList.innerHTML = html;
|
||
|
||
// Populate restore dropdown
|
||
const restoreSelect = document.getElementById('restore-backup-select');
|
||
if (restoreSelect) {
|
||
restoreSelect.innerHTML = '<option value="">-- Choose a backup to restore --</option>';
|
||
data.backups.forEach(backup => {
|
||
restoreSelect.innerHTML += `<option value="${backup.filename}">${backup.filename} (${backup.size_mb} MB - ${backup.created})</option>`;
|
||
});
|
||
}
|
||
} else {
|
||
// Update count badge
|
||
document.getElementById('backup-count-badge').textContent = '0';
|
||
|
||
backupList.innerHTML = `
|
||
<div style="text-align: center; padding: 40px 20px; color: var(--text-secondary, #999);">
|
||
<div style="font-size: 3em; margin-bottom: 10px;">📂</div>
|
||
<p style="margin: 0;">No backups available</p>
|
||
<small>Create your first backup using the buttons above</small>
|
||
</div>
|
||
`;
|
||
|
||
// Clear restore dropdown
|
||
const restoreSelect = document.getElementById('restore-backup-select');
|
||
if (restoreSelect) {
|
||
restoreSelect.innerHTML = '<option value="">-- No backups available --</option>';
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading backups:', error);
|
||
backupList.innerHTML = `
|
||
<div style="text-align: center; padding: 40px 20px; color: #f44336;">
|
||
<div style="font-size: 3em; margin-bottom: 10px;">⚠️</div>
|
||
<p style="margin: 0; font-weight: 600;">Error loading backups</p>
|
||
<small>Please try refreshing the page</small>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// ========================================
|
||
// Log Cleanup Management Functions
|
||
// ========================================
|
||
|
||
// Load log cleanup settings
|
||
function loadLogCleanupSettings() {
|
||
fetch('/api/maintenance/log-settings')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && data.retention_days !== undefined) {
|
||
document.getElementById('log-retention-days').value = data.retention_days;
|
||
}
|
||
})
|
||
.catch(error => console.error('Error loading log settings:', error));
|
||
}
|
||
|
||
// Save log cleanup settings
|
||
document.getElementById('save-log-settings-btn')?.addEventListener('click', function() {
|
||
const retentionDays = document.getElementById('log-retention-days').value;
|
||
const statusDiv = document.getElementById('log-cleanup-status');
|
||
const messageSpan = document.getElementById('log-cleanup-message');
|
||
|
||
fetch('/api/maintenance/log-settings', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
retention_days: parseInt(retentionDays)
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
statusDiv.style.display = 'block';
|
||
if (data.success) {
|
||
statusDiv.style.background = '#e8f5e9';
|
||
statusDiv.style.borderLeftColor = '#4caf50';
|
||
messageSpan.textContent = '✅ ' + data.message;
|
||
} else {
|
||
statusDiv.style.background = '#ffebee';
|
||
statusDiv.style.borderLeftColor = '#f44336';
|
||
messageSpan.textContent = '❌ ' + data.message;
|
||
}
|
||
setTimeout(() => {
|
||
statusDiv.style.display = 'none';
|
||
}, 5000);
|
||
})
|
||
.catch(error => {
|
||
console.error('Error saving log settings:', error);
|
||
statusDiv.style.display = 'block';
|
||
statusDiv.style.background = '#ffebee';
|
||
statusDiv.style.borderLeftColor = '#f44336';
|
||
messageSpan.textContent = '❌ Error saving settings';
|
||
});
|
||
});
|
||
|
||
// Clean up logs now
|
||
document.getElementById('cleanup-logs-now-btn')?.addEventListener('click', function() {
|
||
if (!confirm('Are you sure you want to clean up old log files now?\n\nThis will delete log files older than the configured retention period.')) {
|
||
return;
|
||
}
|
||
|
||
const btn = this;
|
||
const originalText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Cleaning...';
|
||
|
||
const statusDiv = document.getElementById('log-cleanup-status');
|
||
const messageSpan = document.getElementById('log-cleanup-message');
|
||
|
||
fetch('/api/maintenance/cleanup-logs', {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
statusDiv.style.display = 'block';
|
||
|
||
if (data.success) {
|
||
statusDiv.style.background = '#e8f5e9';
|
||
statusDiv.style.borderLeftColor = '#4caf50';
|
||
messageSpan.textContent = '✅ ' + data.message + (data.files_deleted ? ` (${data.files_deleted} files deleted)` : '');
|
||
} else {
|
||
statusDiv.style.background = '#ffebee';
|
||
statusDiv.style.borderLeftColor = '#f44336';
|
||
messageSpan.textContent = '❌ ' + data.message;
|
||
}
|
||
|
||
setTimeout(() => {
|
||
statusDiv.style.display = 'none';
|
||
}, 8000);
|
||
})
|
||
.catch(error => {
|
||
console.error('Error cleaning logs:', error);
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
statusDiv.style.display = 'block';
|
||
statusDiv.style.background = '#ffebee';
|
||
statusDiv.style.borderLeftColor = '#f44336';
|
||
messageSpan.textContent = '❌ Error cleaning logs';
|
||
});
|
||
});
|
||
|
||
// Per-Table Backup & Restore
|
||
let tablesForBackup = [];
|
||
let tableBackupsData = [];
|
||
|
||
// Toggle table backup section
|
||
document.getElementById('toggle-table-backup-btn')?.addEventListener('click', function() {
|
||
const section = document.getElementById('table-backup-section');
|
||
const textSpan = document.getElementById('toggle-table-backup-text');
|
||
const arrow = this.querySelector('span:last-child');
|
||
|
||
if (section.style.display === 'none') {
|
||
section.style.display = 'block';
|
||
textSpan.textContent = 'Hide Per-Table Backup & Restore';
|
||
arrow.textContent = '▲';
|
||
loadTablesForBackup();
|
||
loadTableBackups();
|
||
} else {
|
||
section.style.display = 'none';
|
||
textSpan.textContent = 'Show Per-Table Backup & Restore';
|
||
arrow.textContent = '▼';
|
||
}
|
||
});
|
||
|
||
// Load tables for backup dropdown
|
||
function loadTablesForBackup() {
|
||
fetch('/api/maintenance/database-tables')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && data.tables.length > 0) {
|
||
tablesForBackup = data.tables;
|
||
const select = document.getElementById('table-backup-select');
|
||
select.innerHTML = '<option value="">-- Select table --</option>';
|
||
|
||
data.tables.forEach(table => {
|
||
select.innerHTML += `<option value="${table.name}">${table.name} (${table.rows} rows)</option>`;
|
||
});
|
||
}
|
||
})
|
||
.catch(error => console.error('Error loading tables:', error));
|
||
}
|
||
|
||
// Load available table backups
|
||
function loadTableBackups() {
|
||
fetch('/api/backup/table-backups')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && data.backups.length > 0) {
|
||
tableBackupsData = data.backups;
|
||
const select = document.getElementById('table-restore-backup-select');
|
||
select.innerHTML = '<option value="">-- Select backup --</option>';
|
||
|
||
data.backups.forEach(backup => {
|
||
select.innerHTML += `<option value="${backup.filename}">${backup.table_name} - ${backup.created} (${backup.size})</option>`;
|
||
});
|
||
}
|
||
})
|
||
.catch(error => console.error('Error loading table backups:', error));
|
||
}
|
||
|
||
// Enable/disable backup button based on selection
|
||
document.getElementById('table-backup-select')?.addEventListener('change', function() {
|
||
document.getElementById('backup-single-table-btn').disabled = !this.value;
|
||
});
|
||
|
||
// Enable/disable restore button based on selection
|
||
document.getElementById('table-restore-backup-select')?.addEventListener('change', function() {
|
||
document.getElementById('restore-single-table-btn').disabled = !this.value;
|
||
});
|
||
|
||
// Backup single table
|
||
document.getElementById('backup-single-table-btn')?.addEventListener('click', function() {
|
||
const tableName = document.getElementById('table-backup-select').value;
|
||
|
||
if (!tableName) {
|
||
showTableBackupStatus('❌ Please select a table', 'error');
|
||
return;
|
||
}
|
||
|
||
const btn = this;
|
||
const originalText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Backing up...';
|
||
|
||
fetch('/api/backup/table', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
table_name: tableName
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
|
||
if (data.success) {
|
||
showTableBackupStatus('✅ ' + data.message, 'success');
|
||
loadTableBackups(); // Refresh backup list
|
||
document.getElementById('table-backup-select').value = '';
|
||
btn.disabled = true;
|
||
} else {
|
||
showTableBackupStatus('❌ ' + data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error backing up table:', error);
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
showTableBackupStatus('❌ Error backing up table', 'error');
|
||
});
|
||
});
|
||
|
||
// Restore single table
|
||
document.getElementById('restore-single-table-btn')?.addEventListener('click', function() {
|
||
const backupFile = document.getElementById('table-restore-backup-select').value;
|
||
|
||
if (!backupFile) {
|
||
showTableBackupStatus('❌ Please select a backup', 'error');
|
||
return;
|
||
}
|
||
|
||
const backup = tableBackupsData.find(b => b.filename === backupFile);
|
||
const tableName = backup ? backup.table_name : 'unknown';
|
||
|
||
if (!confirm(`⚠️ WARNING: This will replace all data in the "${tableName}" table!\n\nThis action CANNOT be undone!\n\nAre you sure you want to continue?`)) {
|
||
return;
|
||
}
|
||
|
||
const btn = this;
|
||
const originalText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Restoring...';
|
||
|
||
fetch('/api/restore/table', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
backup_file: backupFile
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
|
||
if (data.success) {
|
||
showTableBackupStatus('✅ ' + data.message, 'success');
|
||
document.getElementById('table-restore-backup-select').value = '';
|
||
btn.disabled = true;
|
||
} else {
|
||
showTableBackupStatus('❌ ' + data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error restoring table:', error);
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
showTableBackupStatus('❌ Error restoring table', 'error');
|
||
});
|
||
});
|
||
|
||
function showTableBackupStatus(message, type) {
|
||
const statusDiv = document.getElementById('table-backup-status');
|
||
const messageSpan = document.getElementById('table-backup-message');
|
||
|
||
statusDiv.style.display = 'block';
|
||
messageSpan.textContent = message;
|
||
|
||
if (type === 'success') {
|
||
statusDiv.style.background = 'var(--status-success-bg, #e8f5e9)';
|
||
statusDiv.style.borderLeftColor = 'var(--status-success-border, #4caf50)';
|
||
} else {
|
||
statusDiv.style.background = 'var(--status-error-bg, #ffebee)';
|
||
statusDiv.style.borderLeftColor = 'var(--status-error-border, #f44336)';
|
||
}
|
||
|
||
setTimeout(() => {
|
||
statusDiv.style.display = 'none';
|
||
}, 8000);
|
||
}
|
||
|
||
// Database Table Management
|
||
let tablesData = [];
|
||
|
||
// Load database tables
|
||
document.getElementById('load-tables-btn')?.addEventListener('click', function() {
|
||
const btn = this;
|
||
const originalText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Loading...';
|
||
|
||
fetch('/api/maintenance/database-tables')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
|
||
if (data.success && data.tables.length > 0) {
|
||
tablesData = data.tables;
|
||
const select = document.getElementById('table-to-drop');
|
||
select.innerHTML = '<option value="">-- Select a table --</option>';
|
||
|
||
data.tables.forEach(table => {
|
||
select.innerHTML += `<option value="${table.name}">${table.name} (${table.rows} rows, ${table.size})</option>`;
|
||
});
|
||
|
||
document.getElementById('tables-list-container').style.display = 'block';
|
||
showTableStatus('✅ Loaded ' + data.tables.length + ' tables', 'success');
|
||
} else {
|
||
showTableStatus('❌ No tables found or error loading tables', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading tables:', error);
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
showTableStatus('❌ Error loading tables', 'error');
|
||
});
|
||
});
|
||
|
||
// Table selection change
|
||
document.getElementById('table-to-drop')?.addEventListener('change', function() {
|
||
const tableName = this.value;
|
||
const dropBtn = document.getElementById('drop-table-btn');
|
||
const infoDiv = document.getElementById('table-info');
|
||
|
||
if (tableName) {
|
||
const tableData = tablesData.find(t => t.name === tableName);
|
||
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;
|
||
}
|
||
} else {
|
||
infoDiv.style.display = 'none';
|
||
dropBtn.disabled = true;
|
||
}
|
||
});
|
||
|
||
// Drop table
|
||
document.getElementById('drop-table-btn')?.addEventListener('click', function() {
|
||
const tableName = document.getElementById('table-to-drop').value;
|
||
|
||
if (!tableName) {
|
||
showTableStatus('❌ Please select a table', 'error');
|
||
return;
|
||
}
|
||
|
||
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` +
|
||
`This action CANNOT be undone!\n\n` +
|
||
`Type the table name to confirm: "${tableName}"`;
|
||
|
||
const userInput = prompt(confirmMessage);
|
||
|
||
if (userInput !== tableName) {
|
||
showTableStatus('❌ Table name did not match. Operation cancelled.', 'error');
|
||
return;
|
||
}
|
||
|
||
const btn = this;
|
||
const originalText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Dropping...';
|
||
|
||
fetch('/api/maintenance/drop-table', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
table_name: tableName
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
|
||
if (data.success) {
|
||
showTableStatus('✅ ' + data.message, 'success');
|
||
// Reset and reload
|
||
document.getElementById('table-to-drop').value = '';
|
||
document.getElementById('table-info').style.display = 'none';
|
||
btn.disabled = true;
|
||
// Reload tables list
|
||
setTimeout(() => {
|
||
document.getElementById('load-tables-btn').click();
|
||
}, 2000);
|
||
} else {
|
||
showTableStatus('❌ ' + data.message, 'error');
|
||
btn.disabled = false;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error dropping table:', error);
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
showTableStatus('❌ Error dropping table', 'error');
|
||
});
|
||
});
|
||
|
||
function showTableStatus(message, type) {
|
||
const statusDiv = document.getElementById('table-operation-status');
|
||
const messageSpan = document.getElementById('table-operation-message');
|
||
|
||
statusDiv.style.display = 'block';
|
||
messageSpan.textContent = message;
|
||
|
||
if (type === 'success') {
|
||
statusDiv.style.background = 'var(--status-success-bg, #e8f5e9)';
|
||
statusDiv.style.borderLeftColor = 'var(--status-success-border, #4caf50)';
|
||
} else {
|
||
statusDiv.style.background = 'var(--status-error-bg, #ffebee)';
|
||
statusDiv.style.borderLeftColor = 'var(--status-error-border, #f44336)';
|
||
}
|
||
|
||
setTimeout(() => {
|
||
statusDiv.style.display = 'none';
|
||
}, 8000);
|
||
}
|
||
|
||
// Load storage information
|
||
function loadStorageInfo() {
|
||
fetch('/api/maintenance/storage-info')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
document.getElementById('logs-size').textContent = data.logs_size;
|
||
document.getElementById('database-size').textContent = data.database_size;
|
||
document.getElementById('backups-size').textContent = data.backups_size;
|
||
} else {
|
||
document.getElementById('logs-size').textContent = 'Error';
|
||
document.getElementById('database-size').textContent = 'Error';
|
||
document.getElementById('backups-size').textContent = 'Error';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading storage info:', error);
|
||
document.getElementById('logs-size').textContent = 'Error';
|
||
document.getElementById('database-size').textContent = 'Error';
|
||
document.getElementById('backups-size').textContent = 'Error';
|
||
});
|
||
}
|
||
|
||
// Refresh storage information
|
||
document.getElementById('refresh-storage-btn')?.addEventListener('click', function() {
|
||
const btn = this;
|
||
const originalText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Loading...';
|
||
|
||
loadStorageInfo();
|
||
|
||
setTimeout(() => {
|
||
btn.disabled = false;
|
||
btn.textContent = originalText;
|
||
}, 1000);
|
||
});
|
||
|
||
// Load settings on page load
|
||
if (document.getElementById('log-retention-days')) {
|
||
loadLogCleanupSettings();
|
||
loadStorageInfo();
|
||
}
|
||
|
||
// Backup now button
|
||
document.getElementById('backup-now-btn')?.addEventListener('click', function() {
|
||
const btn = this;
|
||
const originalHTML = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.innerHTML = `
|
||
<span class="btn-icon">⏳</span>
|
||
<span class="btn-text">
|
||
<strong>Creating...</strong>
|
||
<small>Please wait</small>
|
||
</span>
|
||
`;
|
||
|
||
fetch('/api/backup/create', {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert('✅ ' + data.message + '\n\nFile: ' + data.filename + '\nSize: ' + data.size);
|
||
loadBackupList();
|
||
} else {
|
||
alert('❌ ' + data.message);
|
||
}
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalHTML;
|
||
})
|
||
.catch(error => {
|
||
console.error('Error creating backup:', error);
|
||
alert('❌ Failed to create backup');
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalHTML;
|
||
});
|
||
});
|
||
|
||
// Data-only backup button
|
||
document.getElementById('backup-data-only-btn')?.addEventListener('click', function() {
|
||
const btn = this;
|
||
const originalHTML = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.innerHTML = `
|
||
<span class="btn-icon">⏳</span>
|
||
<span class="btn-text">
|
||
<strong>Creating...</strong>
|
||
<small>Please wait</small>
|
||
</span>
|
||
`;
|
||
|
||
fetch('/api/backup/create-data-only', {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert('✅ ' + data.message + '\n\nFile: ' + data.filename + '\nSize: ' + data.size + '\n\n📦 This backup contains only data (no schema or triggers)');
|
||
loadBackupList();
|
||
} else {
|
||
alert('❌ ' + data.message);
|
||
}
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalHTML;
|
||
})
|
||
.catch(error => {
|
||
console.error('Error creating data backup:', error);
|
||
alert('❌ Failed to create data backup');
|
||
btn.disabled = false;
|
||
btn.innerHTML = originalHTML;
|
||
});
|
||
});
|
||
|
||
// Refresh backups button
|
||
document.getElementById('refresh-backups-btn')?.addEventListener('click', function() {
|
||
loadBackupList();
|
||
});
|
||
|
||
// Add schedule button - show form
|
||
document.getElementById('add-schedule-btn')?.addEventListener('click', function() {
|
||
// Clear form
|
||
document.getElementById('schedule-id').value = '';
|
||
document.getElementById('schedule-name').value = '';
|
||
document.getElementById('schedule-enabled').checked = true;
|
||
document.getElementById('schedule-time').value = '02:00';
|
||
document.getElementById('schedule-frequency').value = 'daily';
|
||
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';
|
||
document.getElementById('backup-schedule-form').style.display = 'block';
|
||
});
|
||
|
||
// Edit schedule function
|
||
function editSchedule(scheduleId) {
|
||
fetch('/api/backup/schedule')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const schedule = data.schedule.schedules.find(s => s.id === scheduleId);
|
||
if (schedule) {
|
||
// Populate form
|
||
document.getElementById('schedule-id').value = schedule.id;
|
||
document.getElementById('schedule-name').value = schedule.name || schedule.id;
|
||
document.getElementById('schedule-enabled').checked = schedule.enabled;
|
||
document.getElementById('schedule-time').value = schedule.time;
|
||
document.getElementById('schedule-frequency').value = schedule.frequency;
|
||
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';
|
||
}
|
||
}
|
||
})
|
||
.catch(error => console.error('Error loading schedule:', error));
|
||
}
|
||
|
||
// Toggle schedule function
|
||
function toggleSchedule(scheduleId) {
|
||
if (!confirm(`Are you sure you want to toggle this schedule ${scheduleId}?`)) {
|
||
return;
|
||
}
|
||
|
||
fetch(`/api/backup/schedule/toggle/${scheduleId}`, {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert('✅ ' + data.message);
|
||
loadBackupSchedule();
|
||
} else {
|
||
alert('❌ ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error toggling schedule:', error);
|
||
alert('❌ Failed to toggle schedule');
|
||
});
|
||
}
|
||
|
||
// Delete schedule function
|
||
function deleteSchedule(scheduleId) {
|
||
if (!confirm(`Are you sure you want to delete schedule "${scheduleId}"?\n\nThis action cannot be undone.`)) {
|
||
return;
|
||
}
|
||
|
||
fetch(`/api/backup/schedule/delete/${scheduleId}`, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert('✅ ' + data.message);
|
||
loadBackupSchedule();
|
||
} else {
|
||
alert('❌ ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error deleting schedule:', error);
|
||
alert('❌ Failed to delete schedule');
|
||
});
|
||
}
|
||
|
||
// 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';
|
||
});
|
||
|
||
// Save schedule form
|
||
document.getElementById('backup-schedule-form')?.addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
|
||
const scheduleId = document.getElementById('schedule-id').value;
|
||
const isEdit = scheduleId !== '';
|
||
|
||
const formData = {
|
||
id: scheduleId || undefined,
|
||
name: document.getElementById('schedule-name').value,
|
||
enabled: document.getElementById('schedule-enabled').checked,
|
||
time: document.getElementById('schedule-time').value,
|
||
frequency: document.getElementById('schedule-frequency').value,
|
||
backup_type: document.getElementById('schedule-backup-type').value,
|
||
retention_days: parseInt(document.getElementById('retention-days').value)
|
||
};
|
||
|
||
// Build informative success message
|
||
const backupTypeText = formData.backup_type === 'data-only' ? 'Data-Only' : 'Full';
|
||
const freqText = formData.frequency.charAt(0).toUpperCase() + formData.frequency.slice(1);
|
||
const scheduleInfo = `${backupTypeText} backups ${freqText} at ${formData.time}`;
|
||
|
||
const endpoint = isEdit ? '/api/backup/schedule' : '/api/backup/schedule/add';
|
||
const method = 'POST';
|
||
|
||
// For edit, we need to update the entire schedule config
|
||
if (isEdit) {
|
||
// Load current config and update the specific schedule
|
||
fetch('/api/backup/schedule')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const config = data.schedule;
|
||
const scheduleIndex = config.schedules.findIndex(s => s.id === scheduleId);
|
||
|
||
if (scheduleIndex >= 0) {
|
||
config.schedules[scheduleIndex] = formData;
|
||
}
|
||
|
||
return fetch('/api/backup/schedule', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(config)
|
||
});
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert(`✅ Schedule updated successfully!\n\n${scheduleInfo}`);
|
||
document.getElementById('backup-schedule-form').style.display = 'none';
|
||
document.getElementById('schedules-list').style.display = 'block';
|
||
loadBackupSchedule();
|
||
} else {
|
||
alert('❌ ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error saving schedule:', error);
|
||
alert('❌ Failed to save schedule');
|
||
});
|
||
} else {
|
||
// Add new schedule
|
||
fetch(endpoint, {
|
||
method: method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(formData)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert(`✅ Schedule added successfully!\n\n${scheduleInfo}`);
|
||
document.getElementById('backup-schedule-form').style.display = 'none';
|
||
document.getElementById('schedules-list').style.display = 'block';
|
||
loadBackupSchedule();
|
||
} else {
|
||
alert('❌ ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error saving schedule:', error);
|
||
alert('❌ Failed to save schedule');
|
||
});
|
||
}
|
||
});
|
||
|
||
// Download backup function
|
||
function downloadBackup(filename) {
|
||
window.location.href = `/api/backup/download/${filename}`;
|
||
}
|
||
|
||
// Delete backup function
|
||
function deleteBackup(filename) {
|
||
if (confirm(`Are you sure you want to delete backup: ${filename}?`)) {
|
||
fetch(`/api/backup/delete/${filename}`, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert('✅ ' + data.message);
|
||
loadBackupList();
|
||
} else {
|
||
alert('❌ ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error deleting backup:', error);
|
||
alert('❌ Failed to delete backup');
|
||
});
|
||
}
|
||
}
|
||
|
||
// Restore dropdown change - enable/disable button
|
||
document.getElementById('restore-backup-select')?.addEventListener('change', function() {
|
||
const restoreBtn = document.getElementById('restore-btn');
|
||
if (this.value) {
|
||
restoreBtn.disabled = false;
|
||
} else {
|
||
restoreBtn.disabled = true;
|
||
}
|
||
});
|
||
|
||
// Restore backup function
|
||
document.getElementById('restore-btn')?.addEventListener('click', function() {
|
||
const filename = document.getElementById('restore-backup-select').value;
|
||
const restoreType = document.querySelector('input[name="restore-type"]:checked').value;
|
||
|
||
if (!filename) {
|
||
alert('❌ Please select a backup file to restore');
|
||
return;
|
||
}
|
||
|
||
// Check if it's a data-only backup being restored as full
|
||
const isDataOnlyFile = filename.includes('data_only_');
|
||
|
||
if (restoreType === 'full' && isDataOnlyFile) {
|
||
alert('⚠️ Warning: You selected a data-only backup file but chose "Full Restore".\n\n' +
|
||
'Data-only backups do not contain database schema or triggers.\n' +
|
||
'Please select "Data-Only Restore" instead, or choose a full backup file.');
|
||
return;
|
||
}
|
||
|
||
// Prepare warning message based on restore type
|
||
let warningMessage, confirmMessage;
|
||
|
||
if (restoreType === 'data-only') {
|
||
warningMessage = `⚠️ DATA-ONLY RESTORE WARNING ⚠️\n\n` +
|
||
`You are about to RESTORE ONLY THE DATA from:\n${filename}\n\n` +
|
||
`This will:\n` +
|
||
`• TRUNCATE all tables (delete all current data)\n` +
|
||
`• INSERT data from backup\n` +
|
||
`• KEEP existing database schema and triggers\n\n` +
|
||
`This action CANNOT be undone!\n\n` +
|
||
`Do you want to continue?`;
|
||
|
||
confirmMessage = `⚠️ FINAL CONFIRMATION ⚠️\n\n` +
|
||
`Type "RESTORE DATA" in capital letters to confirm you understand:\n` +
|
||
`• All current data will be PERMANENTLY DELETED\n` +
|
||
`• Database structure will remain unchanged\n` +
|
||
`• This action is IRREVERSIBLE\n\n` +
|
||
`Type RESTORE DATA to continue:`;
|
||
} else {
|
||
warningMessage = `⚠️ FULL RESTORE WARNING ⚠️\n\n` +
|
||
`You are about to RESTORE the entire database from:\n${filename}\n\n` +
|
||
`This will PERMANENTLY DELETE:\n` +
|
||
`• All current data\n` +
|
||
`• Database schema\n` +
|
||
`• All triggers\n\n` +
|
||
`And replace them with the backup content.\n\n` +
|
||
`This action CANNOT be undone!\n\n` +
|
||
`Do you want to continue?`;
|
||
|
||
confirmMessage = `⚠️ FINAL CONFIRMATION ⚠️\n\n` +
|
||
`Type "RESTORE" in capital letters to confirm you understand:\n` +
|
||
`• All current database data will be PERMANENTLY DELETED\n` +
|
||
`• This action is IRREVERSIBLE\n` +
|
||
`• Users may experience downtime during restore\n\n` +
|
||
`Type RESTORE to continue:`;
|
||
}
|
||
|
||
// First confirmation
|
||
const firstConfirm = confirm(warningMessage);
|
||
|
||
if (!firstConfirm) {
|
||
return;
|
||
}
|
||
|
||
// Second confirmation (require typing confirmation)
|
||
const expectedText = restoreType === 'data-only' ? 'RESTORE DATA' : 'RESTORE';
|
||
const secondConfirm = prompt(confirmMessage);
|
||
|
||
if (secondConfirm !== expectedText) {
|
||
alert('❌ Restore cancelled - confirmation text did not match');
|
||
return;
|
||
}
|
||
|
||
// Perform restore
|
||
const btn = this;
|
||
btn.disabled = true;
|
||
|
||
const restoreEndpoint = restoreType === 'data-only'
|
||
? `/api/backup/restore-data-only/${filename}`
|
||
: `/api/backup/restore/${filename}`;
|
||
|
||
const restoreTypeText = restoreType === 'data-only' ? 'data' : 'database';
|
||
btn.innerHTML = `⏳ Restoring ${restoreTypeText}... Please wait...`;
|
||
|
||
fetch(restoreEndpoint, {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const successMsg = restoreType === 'data-only'
|
||
? `✅ DATA RESTORE COMPLETE!\n\n${data.message}\n\n` +
|
||
`Data has been refreshed. Database schema and triggers remain unchanged.\n\n` +
|
||
`The application will now reload to apply changes.`
|
||
: `✅ DATABASE RESTORE COMPLETE!\n\n${data.message}\n\n` +
|
||
`The application will now reload to apply changes.`;
|
||
|
||
alert(successMsg);
|
||
// Reload the page to ensure all data is fresh
|
||
window.location.reload();
|
||
} else {
|
||
const errorMsg = restoreType === 'data-only'
|
||
? `❌ DATA RESTORE FAILED\n\n${data.message}`
|
||
: `❌ RESTORE FAILED\n\n${data.message}`;
|
||
|
||
alert(errorMsg);
|
||
btn.disabled = false;
|
||
btn.innerHTML = '🔄 Restore Database from Selected Backup';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error restoring backup:', error);
|
||
alert(`❌ RESTORE FAILED\n\nAn error occurred while restoring the ${restoreTypeText}.`);
|
||
btn.disabled = false;
|
||
btn.innerHTML = '🔄 Restore Database from Selected Backup';
|
||
});
|
||
});
|
||
|
||
// Upload backup file
|
||
document.getElementById('upload-backup-btn')?.addEventListener('click', function() {
|
||
const fileInput = document.getElementById('backup-file-upload');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file) {
|
||
alert('❌ Please select a file to upload');
|
||
return;
|
||
}
|
||
|
||
// Validate file extension
|
||
if (!file.name.toLowerCase().endsWith('.sql')) {
|
||
alert('❌ Invalid file format. Only .sql files are allowed.');
|
||
return;
|
||
}
|
||
|
||
// Validate file size (10GB max for large databases)
|
||
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB in bytes
|
||
if (file.size > maxSize) {
|
||
alert('❌ File is too large. Maximum size is 10GB.');
|
||
return;
|
||
}
|
||
|
||
// Warn about large files
|
||
const warningSize = 1 * 1024 * 1024 * 1024; // 1GB
|
||
if (file.size > warningSize) {
|
||
const sizeGB = (file.size / (1024 * 1024 * 1024)).toFixed(2);
|
||
if (!confirm(`⚠️ Large File Warning\n\nYou are uploading a ${sizeGB} GB file.\nThis may take several minutes.\n\nDo you want to continue?`)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Prepare form data
|
||
const formData = new FormData();
|
||
formData.append('backup_file', file);
|
||
|
||
// Disable button and show loading
|
||
const btn = this;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '⏳ Uploading and validating...';
|
||
|
||
// Upload file
|
||
fetch('/api/backup/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Build detailed success message with validation info
|
||
let message = `✅ File uploaded and validated successfully!\n\n`;
|
||
message += `Filename: ${data.filename}\n`;
|
||
message += `Size: ${data.size}\n`;
|
||
|
||
// Add validation details if available
|
||
if (data.validation && data.validation.details) {
|
||
const details = data.validation.details;
|
||
message += `\n📊 Validation Results:\n`;
|
||
message += `• Lines: ${details.line_count || 'N/A'}\n`;
|
||
message += `• Has Users Table: ${details.has_users_table ? '✓' : '✗'}\n`;
|
||
message += `• Has Data: ${details.has_insert_statements ? '✓' : '✗'}\n`;
|
||
}
|
||
|
||
// Add warnings if any
|
||
if (data.validation && data.validation.warnings && data.validation.warnings.length > 0) {
|
||
message += `\n⚠️ Warnings:\n`;
|
||
data.validation.warnings.forEach(warning => {
|
||
message += `• ${warning}\n`;
|
||
});
|
||
}
|
||
|
||
message += `\nThe file is now available in the restore dropdown.`;
|
||
|
||
alert(message);
|
||
|
||
// Clear file input
|
||
fileInput.value = '';
|
||
// Reload backup list to show the new file
|
||
loadBackupList();
|
||
} else {
|
||
// Build detailed error message
|
||
let message = `❌ Upload failed\n\n${data.message}`;
|
||
|
||
// Add validation details if available
|
||
if (data.validation_details) {
|
||
message += `\n\n📊 Validation Details:\n`;
|
||
const details = data.validation_details;
|
||
if (details.size_mb) message += `• File Size: ${details.size_mb} MB\n`;
|
||
if (details.line_count) message += `• Lines: ${details.line_count}\n`;
|
||
}
|
||
|
||
// Add warnings if any
|
||
if (data.warnings && data.warnings.length > 0) {
|
||
message += `\n⚠️ Issues Found:\n`;
|
||
data.warnings.forEach(warning => {
|
||
message += `• ${warning}\n`;
|
||
});
|
||
}
|
||
|
||
alert(message);
|
||
}
|
||
btn.disabled = false;
|
||
btn.innerHTML = '⬆️ Upload File';
|
||
})
|
||
.catch(error => {
|
||
console.error('Error uploading backup:', error);
|
||
alert('❌ Failed to upload file');
|
||
btn.disabled = false;
|
||
btn.innerHTML = '⬆️ Upload File';
|
||
});
|
||
});
|
||
|
||
// Load backup data on page load
|
||
if (document.getElementById('backup-list')) {
|
||
loadBackupSchedule();
|
||
loadBackupList();
|
||
}
|
||
|
||
</script>
|
||
{% endblock %} |