Initial commit: Quality App v2 - FG Scan Module with Reports
This commit is contained in:
266
app/templates/modules/settings/app_keys.html
Normal file
266
app/templates/modules/settings/app_keys.html
Normal file
@@ -0,0 +1,266 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}App Keys Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-key"></i> App Keys Management
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Manage API keys and printer pairing keys for QZ Tray</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="list-group">
|
||||
<a href="{{ url_for('settings.general_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-sliders-h"></i> General Settings
|
||||
</a>
|
||||
<a href="{{ url_for('settings.user_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.app_keys') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="fas fa-key"></i> App Keys
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-cogs"></i> Database Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-database"></i> Database Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<!-- QZ Tray Pairing Keys Section -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-print"></i> QZ Tray Printer Pairing Keys
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ error }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle"></i> {{ success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Generate New Pairing Key Form -->
|
||||
<div class="mb-4 p-3 bg-light rounded">
|
||||
<h6 class="mb-3">Generate New Pairing Key</h6>
|
||||
<form method="POST" action="{{ url_for('settings.generate_pairing_key') }}" class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="printer_name" class="form-label">Printer Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="printer_name" name="printer_name"
|
||||
placeholder="e.g., Label Printer 1" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="validity_days" class="form-label">Validity (days) <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="validity_days" name="validity_days" required>
|
||||
<option value="30">30 Days</option>
|
||||
<option value="60">60 Days</option>
|
||||
<option value="90" selected>90 Days</option>
|
||||
<option value="180">180 Days</option>
|
||||
<option value="365">1 Year</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label"> </label>
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
<i class="fas fa-plus"></i> Generate Key
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Active Pairing Keys Table -->
|
||||
<h6 class="mb-3">Active Pairing Keys</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Printer Name</th>
|
||||
<th>Pairing Key</th>
|
||||
<th>Valid Until</th>
|
||||
<th>Days Remaining</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if pairing_keys %}
|
||||
{% for key in pairing_keys %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ key.printer_name }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-primary">{{ key.pairing_key }}</code>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ms-2"
|
||||
onclick="copyToClipboard('{{ key.pairing_key }}')">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<small>{{ key.valid_until }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% set days_left = key.days_remaining %}
|
||||
{% if days_left > 30 %}
|
||||
<span class="badge bg-success">{{ days_left }} days</span>
|
||||
{% elif days_left > 0 %}
|
||||
<span class="badge bg-warning">{{ days_left }} days</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Expired</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('settings.delete_pairing_key', key_id=key.id) }}"
|
||||
style="display: inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this pairing key?');">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4">
|
||||
<div class="empty-state-message">
|
||||
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
|
||||
<span>No pairing keys found. Create one to enable QZ Tray printing.</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App API Keys Section -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-code"></i> Application API Keys
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Generate New API Key Form -->
|
||||
<div class="mb-4 p-3 bg-light rounded">
|
||||
<h6 class="mb-3">Generate New API Key</h6>
|
||||
<form method="POST" action="{{ url_for('settings.generate_api_key') }}" class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="key_name" class="form-label">Key Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="key_name" name="key_name"
|
||||
placeholder="e.g., External Service API" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="key_type" class="form-label">Key Type <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="key_type" name="key_type" required>
|
||||
<option value="app_key">App Key</option>
|
||||
<option value="external_service">External Service</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label"> </label>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-plus"></i> Generate Key
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Active API Keys Table -->
|
||||
<h6 class="mb-3">Active API Keys</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key Name</th>
|
||||
<th>Key Type</th>
|
||||
<th>API Key</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if api_keys %}
|
||||
{% for key in api_keys %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ key.key_name }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ key.key_type }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-primary">{{ key.api_key[:20] }}...</code>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary ms-2"
|
||||
onclick="copyToClipboard('{{ key.api_key }}')">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<small>{{ key.created_at.strftime('%Y-%m-%d %H:%M') if key.created_at else 'N/A' }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('settings.delete_api_key', key_id=key.id) }}"
|
||||
style="display: inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this API key?');">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4">
|
||||
<div class="empty-state-message">
|
||||
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
|
||||
<span>No API keys found. Create one for external integrations.</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="fas fa-info-circle"></i> <strong>Note:</strong> Keep your API keys secure and never share them publicly.
|
||||
Regenerate keys if you suspect they have been compromised.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('Key copied to clipboard!');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
alert('Failed to copy key');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
75
app/templates/modules/settings/database.html
Normal file
75
app/templates/modules/settings/database.html
Normal file
@@ -0,0 +1,75 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Database Settings{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-database"></i> Database Settings
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="list-group">
|
||||
<a href="{{ url_for('settings.general_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-sliders-h"></i> General Settings
|
||||
</a>
|
||||
<a href="{{ url_for('settings.user_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.app_keys') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-key"></i> App Keys
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-cogs"></i> Database Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="fas fa-database"></i> Database Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Database Configuration</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Database configuration is managed through environment variables and Docker.
|
||||
Please check the .env file for current settings.
|
||||
</div>
|
||||
|
||||
<h6>Current Database Status</h6>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Host</td>
|
||||
<td><code>{{ config.DB_HOST }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Port</td>
|
||||
<td><code>{{ config.DB_PORT }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Database</td>
|
||||
<td><code>{{ config.DB_NAME }}</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
901
app/templates/modules/settings/database_management.html
Normal file
901
app/templates/modules/settings/database_management.html
Normal file
@@ -0,0 +1,901 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Database Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-database"></i> Database Management
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Backup, restore, and manage your database operations</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="list-group">
|
||||
<a href="{{ url_for('settings.general_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-sliders-h"></i> General Settings
|
||||
</a>
|
||||
<a href="{{ url_for('settings.user_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.app_keys') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-key"></i> App Keys
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_management') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="fas fa-cogs"></i> Database Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-database"></i> Database Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<!-- Display Messages -->
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ error }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle"></i> {{ success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Backup Retention Settings Section -->
|
||||
<div class="card shadow-sm mb-4 border-info">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-calendar-times"></i> Backup Retention Policy
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Automatic Cleanup:</strong> Set how long backups should be kept on the server. Older backups will be automatically deleted.
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="POST" action="{{ url_for('settings.save_backup_retention') }}" id="retention-form">
|
||||
<div class="mb-3">
|
||||
<label for="backup-retention-days" class="form-label">Keep Backups For:</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="backup-retention-days" name="retention_days" min="1" max="365" value="{{ backup_retention_days or 30 }}" required>
|
||||
<span class="input-group-text">days</span>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">
|
||||
Backups older than this will be automatically deleted when retention policy is applied.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="auto-cleanup-enabled" name="auto_cleanup" value="1" {{ 'checked' if auto_cleanup else '' }}>
|
||||
<label class="form-check-label" for="auto-cleanup-enabled">
|
||||
Enable automatic cleanup of old backups
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">
|
||||
When enabled, backups exceeding the retention period will be automatically deleted.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Retention Policy
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light border-info">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="fas fa-trash"></i> Manual Cleanup</h6>
|
||||
<p class="text-muted small mb-2">Delete backups older than the retention period right now:</p>
|
||||
<button type="button" class="btn btn-warning btn-sm w-100" id="cleanup-old-backups-btn">
|
||||
<i class="fas fa-broom"></i> Clean Up Old Backups
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled Backups Section -->
|
||||
<hr class="my-4">
|
||||
<h6 class="mb-3"><i class="fas fa-clock"></i> Scheduled Backups</h6>
|
||||
<p class="text-muted small mb-3">Create automatic backups on a schedule that respect your retention policy.</p>
|
||||
|
||||
<!-- New Schedule Form -->
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">Create New Schedule</h6>
|
||||
<form id="schedule-form">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-2">
|
||||
<label for="schedule-name" class="form-label">Schedule Name:</label>
|
||||
<input type="text" class="form-control" id="schedule-name" placeholder="e.g., Daily Backup" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-2">
|
||||
<label for="schedule-frequency" class="form-label">Frequency:</label>
|
||||
<select class="form-select" id="schedule-frequency" required onchange="toggleDayOfWeek()">
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-2">
|
||||
<label for="schedule-time" class="form-label">Time:</label>
|
||||
<input type="time" class="form-control" id="schedule-time" value="02:00" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-2" id="day-of-week-container" style="display: none;">
|
||||
<label for="schedule-day" class="form-label">Day of Week:</label>
|
||||
<select class="form-select" id="schedule-day">
|
||||
<option value="Monday">Monday</option>
|
||||
<option value="Tuesday">Tuesday</option>
|
||||
<option value="Wednesday">Wednesday</option>
|
||||
<option value="Thursday">Thursday</option>
|
||||
<option value="Friday">Friday</option>
|
||||
<option value="Saturday">Saturday</option>
|
||||
<option value="Sunday">Sunday</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-2">
|
||||
<label for="schedule-type" class="form-label">Backup Type:</label>
|
||||
<select class="form-select" id="schedule-type" required>
|
||||
<option value="full">Full Database Backup</option>
|
||||
<option value="data_only">Data Only Backup</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block"> </label>
|
||||
<button type="button" class="btn btn-primary w-100" id="create-schedule-btn" onclick="saveBackupSchedule()">
|
||||
<i class="fas fa-plus"></i> Create Schedule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Schedules List -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Schedule Name</th>
|
||||
<th>Frequency</th>
|
||||
<th>Time</th>
|
||||
<th>Type</th>
|
||||
<th>Last Run</th>
|
||||
<th>Next Run</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="schedules-list">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4">
|
||||
<div class="empty-state-message">
|
||||
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
|
||||
<span>No scheduled backups configured</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup Management Section -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-save"></i> Backup Management
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Backup Information:</strong> Create a complete backup of your database including structure and data. Backups are stored on the server.
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mb-4">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<button type="button" class="btn btn-success w-100" id="backup-full-btn" data-bs-toggle="modal" data-bs-target="#backupModal" data-type="full">
|
||||
<i class="fas fa-database"></i> Full Database Backup
|
||||
</button>
|
||||
<small class="text-muted d-block mt-2">Includes structure and all data</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button type="button" class="btn btn-info w-100" id="backup-data-btn" data-bs-toggle="modal" data-bs-target="#backupModal" data-type="data">
|
||||
<i class="fas fa-box"></i> Data Only Backup
|
||||
</button>
|
||||
<small class="text-muted d-block mt-2">Data without table structure</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Info -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light border-secondary">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="fas fa-hdd"></i> Database Size</h6>
|
||||
<div class="display-6" id="db-size">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light border-secondary">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="fas fa-clock"></i> Last Backup</h6>
|
||||
<div class="display-6 small" id="last-backup">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Backups List -->
|
||||
<h6 class="mb-3">Recent Backups</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Type</th>
|
||||
<th>Size</th>
|
||||
<th>Date Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="backups-list">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4">
|
||||
<div class="empty-state-message">
|
||||
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
|
||||
<span>Loading backups...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Restore Management Section -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-undo"></i> Restore Database
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning mb-4">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Warning:</strong> Restoring a backup will <strong>overwrite</strong> your current database. This action cannot be undone. Always verify you're restoring the correct backup!
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="restore-backup-select" class="form-label">Select Backup to Restore:</label>
|
||||
<select class="form-select" id="restore-backup-select">
|
||||
<option value="">-- Select a backup --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="restore-info" style="display: none;" class="alert alert-info mb-3">
|
||||
<p class="mb-2"><strong>Backup Details:</strong></p>
|
||||
<p class="mb-0"><small id="restore-info-text"></small></p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-warning" id="restore-btn" disabled>
|
||||
<i class="fas fa-undo"></i> Restore from Backup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Truncate Section -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-broom"></i> Clear Table Data
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-danger mb-4">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<strong>Caution:</strong> Clearing a table will <strong>delete all data</strong> from the selected table while preserving its structure. This action cannot be undone!
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="truncate-table-select" class="form-label">Select Table:</label>
|
||||
<select class="form-select" id="truncate-table-select">
|
||||
<option value="">-- Select a table --</option>
|
||||
{% for table in tables %}
|
||||
<option value="{{ table.name }}">{{ table.name }} ({{ table.rows }} rows)</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block"> </label>
|
||||
<button type="button" class="btn btn-danger w-100" id="truncate-btn" disabled data-bs-toggle="modal" data-bs-target="#confirmTruncateModal">
|
||||
<i class="fas fa-trash"></i> Clear Selected Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="truncate-info" style="display: none;" class="alert alert-info mt-3 mb-0">
|
||||
<p class="mb-2"><strong>Table Information:</strong></p>
|
||||
<p class="mb-1"><strong>Name:</strong> <span id="truncate-table-name"></span></p>
|
||||
<p class="mb-0"><strong>Rows to Delete:</strong> <span id="truncate-row-count" class="badge bg-danger"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Data Section -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-file-import"></i> Upload Backup File
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Upload:</strong> Upload SQL backup files (.sql) to store them alongside your automatic backups. You can then restore them using the Restore Database section.
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="import-file" class="form-label">Choose SQL File to Upload:</label>
|
||||
<input type="file" class="form-control" id="import-file" accept=".sql" required>
|
||||
<small class="text-muted d-block mt-2">Supported format: .sql files (e.g., from database exports)</small>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary" id="upload-backup-btn" onclick="uploadBackupFile()">
|
||||
<i class="fas fa-upload"></i> Upload Backup File
|
||||
</button>
|
||||
|
||||
<div id="upload-status" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup Modal -->
|
||||
<div class="modal fade" id="backupModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create Backup</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="backup-form" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="backup-name" class="form-label">Backup Name (Optional):</label>
|
||||
<input type="text" class="form-control" id="backup-name" name="backup_name" placeholder="e.g., Pre-Migration Backup">
|
||||
<small class="text-muted">If empty, a timestamp will be used</small>
|
||||
</div>
|
||||
<input type="hidden" name="backup_type" id="backup-type-input" value="">
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" form="backup-form" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> Create Backup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Truncate Modal -->
|
||||
<div class="modal fade" id="confirmTruncateModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title"><i class="fas fa-exclamation-triangle"></i> Confirm Table Clear</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-2">You are about to <strong>permanently delete all data</strong> from:</p>
|
||||
<p class="bg-light p-2 rounded"><strong id="confirm-table-name"></strong></p>
|
||||
<p class="text-danger"><strong>This action cannot be undone!</strong></p>
|
||||
<p class="small text-muted mb-0">Please ensure you have a backup before proceeding.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirm-truncate-btn">
|
||||
<i class="fas fa-trash"></i> Yes, Clear Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load backups list
|
||||
loadBackupsList();
|
||||
|
||||
// Load schedules list
|
||||
loadBackupSchedules();
|
||||
|
||||
// Cleanup old backups handler
|
||||
const cleanupBtn = document.getElementById('cleanup-old-backups-btn');
|
||||
if (cleanupBtn) {
|
||||
cleanupBtn.addEventListener('click', function() {
|
||||
if (confirm('This will delete all backups older than the retention period. Continue?')) {
|
||||
fetch('{{ url_for("settings.cleanup_old_backups") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Cleanup completed! ' + (data.deleted_count || 0) + ' old backups deleted.');
|
||||
loadBackupsList();
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error cleaning up backups: ' + error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Backup button handlers
|
||||
document.getElementById('backup-full-btn').addEventListener('click', function() {
|
||||
document.getElementById('backup-type-input').value = 'full';
|
||||
});
|
||||
|
||||
document.getElementById('backup-data-btn').addEventListener('click', function() {
|
||||
document.getElementById('backup-type-input').value = 'data';
|
||||
});
|
||||
|
||||
// Backup form submission
|
||||
document.getElementById('backup-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
|
||||
fetch('{{ url_for("settings.create_backup") }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Backup created successfully: ' + data.file);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error creating backup: ' + error);
|
||||
});
|
||||
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('backupModal'));
|
||||
if (modal) modal.hide();
|
||||
});
|
||||
|
||||
// Truncate table handler
|
||||
const truncateSelect = document.getElementById('truncate-table-select');
|
||||
const truncateBtn = document.getElementById('truncate-btn');
|
||||
|
||||
console.log('Initializing truncate handler...');
|
||||
console.log('truncateSelect element:', truncateSelect);
|
||||
console.log('truncateBtn element:', truncateBtn);
|
||||
console.log('truncateBtn.disabled initial value:', truncateBtn ? truncateBtn.disabled : 'N/A');
|
||||
|
||||
if (truncateSelect) {
|
||||
truncateSelect.addEventListener('change', function() {
|
||||
const table = this.value;
|
||||
const option = this.options[this.selectedIndex];
|
||||
|
||||
console.log('=== TRUNCATE HANDLER FIRED ===');
|
||||
console.log('Selected value:', table);
|
||||
console.log('Selected option text:', option.text);
|
||||
console.log('Button disabled before:', truncateBtn.disabled);
|
||||
|
||||
if (table) {
|
||||
console.log('Table selected - enabling button');
|
||||
document.getElementById('truncate-info').style.display = 'block';
|
||||
document.getElementById('truncate-table-name').textContent = table;
|
||||
document.getElementById('truncate-row-count').textContent = option.text.match(/\((\d+)\s*rows\)/)?.[1] || '0';
|
||||
truncateBtn.disabled = false;
|
||||
document.getElementById('confirm-table-name').textContent = table;
|
||||
console.log('Button disabled after setting to false:', truncateBtn.disabled);
|
||||
} else {
|
||||
console.log('No table selected - disabling button');
|
||||
document.getElementById('truncate-info').style.display = 'none';
|
||||
truncateBtn.disabled = true;
|
||||
console.log('Button disabled after setting to true:', truncateBtn.disabled);
|
||||
}
|
||||
});
|
||||
console.log('✓ Change event listener registered on truncate-table-select');
|
||||
} else {
|
||||
console.error('✗ truncate-table-select element not found!');
|
||||
}
|
||||
|
||||
// Confirm truncate
|
||||
document.getElementById('confirm-truncate-btn').addEventListener('click', function() {
|
||||
const table = document.getElementById('truncate-table-select').value;
|
||||
|
||||
// Disable button to prevent multiple clicks
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Clearing...';
|
||||
|
||||
fetch('{{ url_for("settings.truncate_table") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ table: table })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Hide modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmTruncateModal'));
|
||||
if (modal) modal.hide();
|
||||
|
||||
// Show success message
|
||||
alert('Table cleared successfully! Refreshing page...');
|
||||
|
||||
// Refresh the page after a short delay
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 500);
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
// Re-enable button
|
||||
this.disabled = false;
|
||||
this.innerHTML = '<i class="fas fa-trash"></i> Yes, Clear Table';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error clearing table: ' + error);
|
||||
// Re-enable button
|
||||
this.disabled = false;
|
||||
this.innerHTML = '<i class="fas fa-trash"></i> Yes, Clear Table';
|
||||
});
|
||||
});
|
||||
|
||||
// Restore backup handler
|
||||
document.getElementById('restore-backup-select').addEventListener('change', function() {
|
||||
const backup = this.value;
|
||||
if (backup) {
|
||||
document.getElementById('restore-info').style.display = 'block';
|
||||
document.getElementById('restore-info-text').textContent = 'Selected: ' + backup;
|
||||
document.getElementById('restore-btn').disabled = false;
|
||||
} else {
|
||||
document.getElementById('restore-info').style.display = 'none';
|
||||
document.getElementById('restore-btn').disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('restore-btn').addEventListener('click', function() {
|
||||
if (confirm('Are you sure? This will overwrite your current database!')) {
|
||||
const backup = document.getElementById('restore-backup-select').value;
|
||||
|
||||
fetch('{{ url_for("settings.restore_database") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ backup: backup })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Database restored successfully!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error restoring database: ' + error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadBackupsList() {
|
||||
fetch('{{ url_for("settings.get_backups_list") }}')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.backups && data.backups.length > 0) {
|
||||
const tbody = document.getElementById('backups-list');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.backups.forEach(backup => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td><code>${backup.name}</code></td>
|
||||
<td><span class="badge bg-info">${backup.type}</span></td>
|
||||
<td>${backup.size}</td>
|
||||
<td><small>${backup.date}</small></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="downloadBackup('${backup.name}')">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Update restore select
|
||||
const select = document.getElementById('restore-backup-select');
|
||||
select.innerHTML = '<option value="">-- Select a backup --</option>';
|
||||
data.backups.forEach(backup => {
|
||||
const option = document.createElement('option');
|
||||
option.value = backup.name;
|
||||
option.textContent = backup.name + ' (' + backup.size + ')';
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// Update last backup info
|
||||
if (data.backups.length > 0) {
|
||||
document.getElementById('last-backup').textContent = data.backups[0].date;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('backups-list').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4">
|
||||
<div class="empty-state-message">
|
||||
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
|
||||
<span>No backups found. Create one now!</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
// Update DB size
|
||||
if (data.db_size) {
|
||||
document.getElementById('db-size').textContent = data.db_size;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading backups:', error);
|
||||
document.getElementById('backups-list').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-danger">
|
||||
<i class="fas fa-exclamation-circle"></i> Error loading backups
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function downloadBackup(filename) {
|
||||
window.location.href = '{{ url_for("settings.download_backup") }}?file=' + encodeURIComponent(filename);
|
||||
}
|
||||
|
||||
// Schedule management functions
|
||||
function toggleDayOfWeek() {
|
||||
const frequency = document.getElementById('schedule-frequency').value;
|
||||
const dayContainer = document.getElementById('day-of-week-container');
|
||||
dayContainer.style.display = frequency === 'weekly' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function loadBackupSchedules() {
|
||||
fetch('{{ url_for("settings.get_backup_schedules") }}')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const tbody = document.getElementById('schedules-list');
|
||||
|
||||
if (data.schedules && data.schedules.length > 0) {
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.schedules.forEach(schedule => {
|
||||
const lastRun = schedule.last_run ? new Date(schedule.last_run).toLocaleString() : 'Never';
|
||||
const nextRun = schedule.next_run ? new Date(schedule.next_run).toLocaleString() : 'Calculating...';
|
||||
const statusBadge = schedule.is_active ?
|
||||
'<span class="badge bg-success">Active</span>' :
|
||||
'<span class="badge bg-secondary">Inactive</span>';
|
||||
|
||||
const frequencyDisplay = schedule.frequency === 'daily' ? 'Daily' :
|
||||
'Weekly (' + (schedule.day_of_week || 'Not set') + ')';
|
||||
const typeDisplay = schedule.backup_type === 'full' ? 'Full Database' : 'Data Only';
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${schedule.schedule_name}</td>
|
||||
<td>${frequencyDisplay}</td>
|
||||
<td>${schedule.time_of_day}</td>
|
||||
<td><span class="badge bg-secondary">${typeDisplay}</span></td>
|
||||
<td><small>${lastRun}</small></td>
|
||||
<td><small>${nextRun}</small></td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="toggleSchedule(${schedule.id})" title="Enable/Disable">
|
||||
<i class="fas fa-power-off"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteSchedule(${schedule.id})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4">
|
||||
<div class="empty-state-message">
|
||||
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
|
||||
<span>No scheduled backups configured</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading schedules:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function saveBackupSchedule() {
|
||||
const name = document.getElementById('schedule-name').value.trim();
|
||||
const frequency = document.getElementById('schedule-frequency').value;
|
||||
const time = document.getElementById('schedule-time').value;
|
||||
const dayOfWeek = document.getElementById('schedule-day').value;
|
||||
const type = document.getElementById('schedule-type').value;
|
||||
|
||||
if (!name || !time) {
|
||||
alert('Please fill in all required fields');
|
||||
return;
|
||||
}
|
||||
|
||||
if (frequency === 'weekly' && !dayOfWeek) {
|
||||
alert('Please select a day for weekly schedules');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
schedule_name: name,
|
||||
frequency: frequency,
|
||||
day_of_week: frequency === 'weekly' ? dayOfWeek : null,
|
||||
time_of_day: time,
|
||||
backup_type: type
|
||||
};
|
||||
|
||||
fetch('{{ url_for("settings.save_backup_schedule") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Backup schedule created successfully!');
|
||||
document.getElementById('schedule-form').reset();
|
||||
loadBackupSchedules();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error creating schedule: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSchedule(scheduleId) {
|
||||
if (confirm('Are you sure you want to delete this schedule?')) {
|
||||
fetch('{{ url_for("settings.delete_backup_schedule", schedule_id=0) }}'.replace('0', scheduleId), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Schedule deleted successfully!');
|
||||
loadBackupSchedules();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error deleting schedule: ' + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSchedule(scheduleId) {
|
||||
fetch('{{ url_for("settings.toggle_backup_schedule", schedule_id=0) }}'.replace('0', scheduleId), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
loadBackupSchedules();
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error toggling schedule: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function uploadBackupFile() {
|
||||
const fileInput = document.getElementById('import-file');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
alert('Please select a file to upload');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.name.toLowerCase().endsWith('.sql')) {
|
||||
alert('Only .sql files are supported');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const statusDiv = document.getElementById('upload-status');
|
||||
statusDiv.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Uploading...</div>';
|
||||
|
||||
fetch('{{ url_for("settings.upload_backup_file") }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
statusDiv.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> File uploaded successfully: ' + data.filename + ' (' + data.size + ' bytes)</div>';
|
||||
fileInput.value = '';
|
||||
loadBackupsList();
|
||||
} else {
|
||||
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Upload failed: ' + data.error + '</div>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Upload error: ' + error + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Load schedules on page load
|
||||
</script>
|
||||
{% endblock %}
|
||||
80
app/templates/modules/settings/general.html
Normal file
80
app/templates/modules/settings/general.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}General Settings{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-sliders-h"></i> General Settings
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="list-group">
|
||||
<a href="{{ url_for('settings.general_settings') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="fas fa-sliders-h"></i> General Settings
|
||||
</a>
|
||||
<a href="{{ url_for('settings.user_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.app_keys') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-key"></i> App Keys
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-cogs"></i> Database Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-database"></i> Database Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">General Application Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ error }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle"></i> {{ success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url_for('settings.general_settings') }}">
|
||||
<div class="mb-3">
|
||||
<label for="app_name" class="form-label">Application Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="app_name" name="app_name" value="{{ app_name }}" required>
|
||||
<small class="form-text text-muted">This name appears in the header and browser title</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="app_version" class="form-label">Version</label>
|
||||
<input type="text" class="form-control" id="app_version" name="app_version" value="{{ app_version }}" disabled>
|
||||
<small class="form-text text-muted">Version cannot be changed</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="session_timeout" class="form-label">Session Timeout (minutes)</label>
|
||||
<input type="number" class="form-control" id="session_timeout" name="session_timeout" value="{{ session_timeout }}" min="1" required>
|
||||
<small class="form-text text-muted">Time before user session expires</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
50
app/templates/modules/settings/index.html
Normal file
50
app/templates/modules/settings/index.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Settings Module{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-cog"></i> Settings
|
||||
</h1>
|
||||
<p class="text-muted">Configure application settings and preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="list-group">
|
||||
<a href="{{ url_for('settings.general_settings') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="fas fa-sliders-h"></i> General Settings
|
||||
</a>
|
||||
<a href="{{ url_for('settings.user_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.app_keys') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-key"></i> App Keys
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-cogs"></i> Database Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-database"></i> Database Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Settings Overview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
Select a settings category from the left menu to configure.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
190
app/templates/modules/settings/user_form.html
Normal file
190
app/templates/modules/settings/user_form.html
Normal file
@@ -0,0 +1,190 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if user %}Edit User{% else %}Create User{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-user-plus"></i> {% if user %}Edit User{% else %}Create New User{% endif %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Manage user account details and permissions</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="list-group">
|
||||
<a href="{{ url_for('settings.general_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-sliders-h"></i> General Settings
|
||||
</a>
|
||||
<a href="{{ url_for('settings.user_management') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-database"></i> Database
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">{% if user %}Edit User Account{% else %}New User Account{% endif %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ error }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle"></i> {{ success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{% if user %}{{ url_for('settings.edit_user', user_id=user.id) }}{% else %}{{ url_for('settings.create_user') }}{% endif %}" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="username" class="form-label">Username <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
value="{{ user.username if user else '' }}"
|
||||
{% if user %}readonly{% endif %}
|
||||
required>
|
||||
<small class="form-text text-muted">{% if user %}Username cannot be changed{% else %}Unique username for login{% endif %}</small>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
value="{{ user.email if user else '' }}">
|
||||
<small class="form-text text-muted">User's email address</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="full_name" class="form-label">Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="full_name" name="full_name"
|
||||
value="{{ user.full_name if user else '' }}"
|
||||
required>
|
||||
<small class="form-text text-muted">User's display name</small>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="role" class="form-label">Role <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="role" name="role" required>
|
||||
<option value="">-- Select a role --</option>
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.name }}"
|
||||
{% if user and user.role == role.name %}selected{% endif %}>
|
||||
{{ role.name | capitalize }} (Level {{ role.level }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">User's access level</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="password" class="form-label">
|
||||
Password
|
||||
{% if not user %}<span class="text-danger">*</span>{% endif %}
|
||||
</label>
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
{% if not user %}required{% else %}placeholder="Leave blank to keep current password"{% endif %}>
|
||||
<small class="form-text text-muted">
|
||||
{% if user %}Leave blank to keep current password{% else %}Minimum 8 characters{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="confirm_password" class="form-label">
|
||||
Confirm Password
|
||||
{% if not user %}<span class="text-danger">*</span>{% endif %}
|
||||
</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
|
||||
{% if not user %}required{% endif %}>
|
||||
<small class="form-text text-muted">Re-enter password to confirm</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<label for="modules" class="form-label">Module Access <span class="text-danger">*</span></label>
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
{% for module in available_modules %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="module_{{ module }}"
|
||||
name="modules" value="{{ module }}"
|
||||
{% if user and module in user_modules %}checked{% endif %}>
|
||||
<label class="form-check-label" for="module_{{ module }}">
|
||||
<i class="fas fa-{% if module == 'quality' %}check-square{% elif module == 'settings' %}sliders-h{% else %}cube{% endif %}"></i>
|
||||
{{ module | capitalize }} Module
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">Select which modules this user can access</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<label for="is_active" class="form-check-label">
|
||||
<input type="checkbox" class="form-check-input" id="is_active" name="is_active"
|
||||
{% if not user or user.is_active %}checked{% endif %}>
|
||||
<span class="ms-2">Active Account</span>
|
||||
</label>
|
||||
<small class="form-text text-muted d-block">Disabled accounts cannot log in</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> {% if user %}Update User{% else %}Create User{% endif %}
|
||||
</button>
|
||||
<a href="{{ url_for('settings.user_management') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</a>
|
||||
{% if user %}
|
||||
<button type="button" class="btn btn-danger ms-auto" onclick="confirmDelete()">
|
||||
<i class="fas fa-trash"></i> Delete User
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user %}
|
||||
<script>
|
||||
function confirmDelete() {
|
||||
if (confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{{ url_for("settings.delete_user", user_id=user.id) }}';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = '_method';
|
||||
input.value = 'DELETE';
|
||||
form.appendChild(input);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
104
app/templates/modules/settings/users.html
Normal file
104
app/templates/modules/settings/users.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}User Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="list-group">
|
||||
<a href="{{ url_for('settings.general_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-sliders-h"></i> General Settings
|
||||
</a>
|
||||
<a href="{{ url_for('settings.user_management') }}" class="list-group-item list-group-item-action active">
|
||||
<i class="fas fa-users"></i> User Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.app_keys') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-key"></i> App Keys
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_management') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-cogs"></i> Database Management
|
||||
</a>
|
||||
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
|
||||
<i class="fas fa-database"></i> Database Info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">User Accounts</h5>
|
||||
<a href="{{ url_for('settings.create_user') }}" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-plus"></i> Create User
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Full Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if users %}
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td><code>{{ user.username }}</code></td>
|
||||
<td>{{ user.full_name }}</td>
|
||||
<td>{% if user.email %}<small>{{ user.email }}</small>{% else %}<small class="text-muted">N/A</small>{% endif %}</td>
|
||||
<td>
|
||||
<span class="badge bg-info text-dark">
|
||||
{{ user.role | capitalize }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{{ url_for('settings.edit_user', user_id=user.id) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4">
|
||||
<div class="empty-state-message">
|
||||
<i class="fas fa-inbox" style="font-size: 24px; margin-right: 10px;"></i>
|
||||
<span>No users found. <a href="{{ url_for('settings.create_user') }}">Create one</a></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user