Files
quality_app/py_app/app/templates/settings.html
2025-12-01 23:48:09 +02:00

2712 lines
105 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Settings{% endblock %}
{% block content %}
<div class="card-container">
<div class="card">
<h3>Manage Users (Legacy)</h3>
<ul class="user-list">
{% for user in users %}
<li data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">
<span class="user-name">{{ user.username }}</span>
<span class="user-role">Role: {{ user.role }}</span>
<button class="btn edit-user-btn" data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">Edit User</button>
<button class="btn delete-btn delete-user-btn" data-user-id="{{ user.id }}" data-username="{{ user.username }}">Delete User</button>
</li>
{% endfor %}
</ul>
<button id="create-user-btn" class="btn create-btn">Create User</button>
</div>
<div class="card">
<h3>External Server Settings</h3>
<form method="POST" action="{{ url_for('main.save_external_db') }}" class="form-centered">
<label for="db_server_domain">Server Domain/IP Address:</label>
<input type="text" id="db_server_domain" name="server_domain" value="{{ external_settings.get('server_domain', '') }}" required>
<label for="db_port">Port:</label>
<input type="number" id="db_port" name="port" value="{{ external_settings.get('port', '') }}" required>
<label for="db_database_name">Database Name:</label>
<input type="text" id="db_database_name" name="database_name" value="{{ external_settings.get('database_name', '') }}" required>
<label for="db_username">Username:</label>
<input type="text" id="db_username" name="username" value="{{ external_settings.get('username', '') }}" required>
<label for="db_password">Password:</label>
<input type="password" id="db_password" name="password" value="{{ external_settings.get('password', '') }}" required>
<button type="submit" class="btn">Save/Update External Database Info Settings</button>
</form>
</div>
<div class="card" style="margin-top: 32px;">
<h3>🎯 User & Permissions Management</h3>
<p><strong>Simplified 4-Tier System:</strong> Superadmin → Admin → Manager → Worker</p>
<p>Streamlined interface with module-based permissions (Quality, Warehouse, Labels)</p>
<div style="margin-top: 15px;">
<a href="{{ url_for('main.user_management_simple') }}" class="btn" style="background-color: #2196f3; color: white; margin-right: 10px;">
🎯 Manage Users (Simplified)
</a>
</div>
<small style="display: block; margin-top: 10px; color: #666;">
Recommended: Use the simplified user management for easier administration
</small>
</div>
{% if session.role == '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 %}