Files
quality_app/py_app/app/templates/settings.html
2025-11-05 21:25:02 +02:00

1966 lines
69 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 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 %}