- Added daily_mirror module to permissions system - Fixed user module management - updates now work correctly - Implemented dashboard module filtering based on user permissions - Fixed warehouse create_locations page (config parser and delete) - Implemented POST-Redirect-GET pattern to prevent duplicate entries - Added application license system with validation middleware - Cleaned up debug logging code - Improved user module selection with fetch API instead of form submit
1982 lines
69 KiB
HTML
Executable File
1982 lines
69 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 backup-card" style="margin-top: 32px;">
|
||
<h3>💾 Database Backup Management</h3>
|
||
<p><strong>Automated Backup System:</strong> Schedule and manage database backups</p>
|
||
{% 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;">
|
||
💾 Database Backup Management
|
||
<span style="font-size: 0.7em; background: #4caf50; color: white; padding: 4px 12px; border-radius: 12px; font-weight: normal;">Active</span>
|
||
</h3>
|
||
|
||
<!-- Grid Layout for Cards -->
|
||
<div class="backup-grid">
|
||
|
||
<!-- Quick Actions Card -->
|
||
<div class="backup-sub-card">
|
||
<div class="sub-card-header">
|
||
<h4>⚡ Quick Actions</h4>
|
||
</div>
|
||
<div class="sub-card-body">
|
||
<button id="backup-now-btn" class="backup-action-btn backup-btn-full">
|
||
<span class="btn-icon">🗄️</span>
|
||
<span class="btn-text">
|
||
<strong>Full Backup</strong>
|
||
<small>Schema + Data + Triggers</small>
|
||
</span>
|
||
</button>
|
||
<button id="backup-data-only-btn" class="backup-action-btn backup-btn-data">
|
||
<span class="btn-icon">📦</span>
|
||
<span class="btn-text">
|
||
<strong>Data-Only Backup</strong>
|
||
<small>Table data only (faster)</small>
|
||
</span>
|
||
</button>
|
||
<button id="refresh-backups-btn" class="backup-action-btn backup-btn-refresh">
|
||
<span class="btn-icon">🔄</span>
|
||
<span class="btn-text">
|
||
<strong>Refresh List</strong>
|
||
<small>Reload backup files</small>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Schedule Card -->
|
||
<div class="backup-sub-card">
|
||
<div class="sub-card-header">
|
||
<h4>⏰ Automatic Schedules</h4>
|
||
<div style="display: flex; gap: 8px; align-items: center;">
|
||
<span id="schedule-count-badge" class="count-badge">0</span>
|
||
<button id="add-schedule-btn" class="btn-small" style="background: #4caf50; color: white; padding: 4px 12px; border-radius: 4px; font-size: 0.85em;">
|
||
➕ Add Schedule
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="sub-card-body">
|
||
<!-- Schedules List -->
|
||
<div id="schedules-list"></div>
|
||
|
||
<!-- Add/Edit Schedule Form (Initially Hidden) -->
|
||
<form id="backup-schedule-form" class="schedule-compact-form" style="display: none;">
|
||
<input type="hidden" id="schedule-id" name="id">
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="schedule-name">Schedule Name</label>
|
||
<input type="text" id="schedule-name" name="name" placeholder="e.g., Monthly Full Backup" required>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="schedule-enabled" name="enabled" checked>
|
||
<span>Enable this schedule</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-row-split">
|
||
<div class="form-group">
|
||
<label for="schedule-time">Time</label>
|
||
<input type="time" id="schedule-time" name="time" value="02:00" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="schedule-frequency">Frequency</label>
|
||
<select id="schedule-frequency" name="frequency" required>
|
||
<option value="daily">Daily</option>
|
||
<option value="weekly">Weekly (Sunday)</option>
|
||
<option value="monthly">Monthly (1st)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row-split">
|
||
<div class="form-group">
|
||
<label for="schedule-backup-type">Backup Type</label>
|
||
<select id="schedule-backup-type" name="backup_type" required>
|
||
<option value="full">Full (Complete)</option>
|
||
<option value="data-only">Data-Only (Fast)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="retention-days">Keep (days)</label>
|
||
<input type="number" id="retention-days" name="retention_days" value="30" min="1" max="365" required>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="submit" class="btn-save">💾 Save Schedule</button>
|
||
<button type="button" id="cancel-schedule-btn" class="btn-cancel">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Available Backups Section -->
|
||
<div class="backup-sub-card" style="margin-top: 20px;">
|
||
<div class="sub-card-header">
|
||
<h4>📂 Available Backups</h4>
|
||
<span id="backup-count-badge" class="count-badge">0</span>
|
||
</div>
|
||
<div class="sub-card-body">
|
||
<div id="backup-list" class="backup-list-modern">
|
||
<div class="backup-loading">
|
||
<div class="loading-spinner"></div>
|
||
<p>Loading backups...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restore Section (Superadmin Only) -->
|
||
{% if current_user.role == 'superadmin' %}
|
||
<div class="backup-sub-card restore-card" style="margin-top: 20px;">
|
||
<div class="sub-card-header" style="background: #fff3e0; color: #e65100;">
|
||
<h4>🔄 Restore Database</h4>
|
||
<span class="warning-badge">⚠️ Danger Zone</span>
|
||
</div>
|
||
<div class="sub-card-body">
|
||
<div class="warning-message">
|
||
<strong>⚠️ Warning:</strong> Restoring will permanently replace ALL current data. This action cannot be undone!
|
||
</div>
|
||
|
||
<!-- Upload Section -->
|
||
<div class="upload-section">
|
||
<label class="upload-label">📤 Upload External Backup</label>
|
||
<div class="upload-row">
|
||
<input type="file" id="backup-file-upload" accept=".sql" class="file-input">
|
||
<button id="upload-backup-btn" class="btn-upload">⬆️ Upload</button>
|
||
</div>
|
||
<small>SQL files only • Max 10GB</small>
|
||
</div>
|
||
|
||
<!-- Restore Section -->
|
||
<div class="restore-section-compact">
|
||
<div class="form-group">
|
||
<label for="restore-backup-select">Select Backup File</label>
|
||
<select id="restore-backup-select" class="restore-select">
|
||
<option value="">-- Choose a backup to restore --</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Restore Type</label>
|
||
<div class="restore-type-options">
|
||
<label class="radio-card">
|
||
<input type="radio" name="restore-type" value="full" checked>
|
||
<div class="radio-content">
|
||
<strong>🔄 Full Restore</strong>
|
||
<small>Replace everything (schema + data + triggers)</small>
|
||
</div>
|
||
</label>
|
||
<label class="radio-card">
|
||
<input type="radio" name="restore-type" value="data-only">
|
||
<div class="radio-content">
|
||
<strong>📦 Data-Only</strong>
|
||
<small>Keep schema, replace data only</small>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<button id="restore-btn" class="btn-restore" disabled>
|
||
🔄 Restore Database
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Backup Path Info -->
|
||
<div class="backup-info-compact">
|
||
<strong>💾 Backup Location:</strong> <code>/srv/quality_app/backups</code>
|
||
<small>Configure in docker(BACKUP_PATH)</small>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
/* Modern Backup Card Styles */
|
||
.backup-card {
|
||
background: var(--card-bg, #fff);
|
||
padding: 24px;
|
||
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(300px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
</style>
|
||
{% endif %}
|
||
</div>
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
</style>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 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>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// 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 %} |