Initial clean commit
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Backup Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-6xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-navy-800">System Backup Manager</h2>
|
||||
<div class="text-sm text-navy-600">{{ version_info.version }} - {{ version_info.release_name }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Card -->
|
||||
<div class="mb-8 p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex flex-wrap md:flex-nowrap gap-4">
|
||||
<div class="flex-1 border rounded-lg p-4 bg-white">
|
||||
<h3 class="text-lg font-semibold mb-2 text-navy-700">Backup Status</h3>
|
||||
<div class="space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Total backups:</span>
|
||||
<span class="font-medium">{{ backup_count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Latest backup:</span>
|
||||
<span class="font-medium">{{ latest_backup|default('None yet') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Scheduled backups:</span>
|
||||
<span class="font-medium">{{ 'Enabled' if schedule_enabled else 'Disabled' }}</span>
|
||||
</div>
|
||||
{% if schedule_enabled %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Next backup:</span>
|
||||
<span class="font-medium">{{ next_backup|default('Not scheduled') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Storage location:</span>
|
||||
<span class="font-medium text-xs md:text-sm font-mono">{{ backup_location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 border rounded-lg p-4 bg-white">
|
||||
<h3 class="text-lg font-semibold mb-2 text-navy-700">Quick Actions</h3>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<form method="POST" action="{{ url_for('users.create_backup') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="w-full bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded flex items-center justify-center">
|
||||
<i class="fas fa-save mr-2"></i> Create New Backup
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a href="#backup-list" class="w-full bg-navy-600 hover:bg-navy-700 text-white py-2 px-4 rounded flex items-center justify-center">
|
||||
<i class="fas fa-list mr-2"></i> Manage Backups
|
||||
</a>
|
||||
|
||||
<button type="button" id="schedule-btn" onclick="toggleSchedulerForm()" class="w-full bg-gray-500 hover:bg-gray-600 text-white py-2 px-4 rounded flex items-center justify-center">
|
||||
<i class="fas fa-clock mr-2"></i> Schedule Backups
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup Schedule Configuration -->
|
||||
<div class="mb-8 p-4 border border-navy-200 rounded-lg {% if not show_schedule_form %}hidden{% endif %}" id="scheduler-form">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Backup Schedule Configuration</h3>
|
||||
|
||||
<form action="{{ url_for('users.schedule_backup') }}" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block font-medium mb-1" for="schedule-time">Time of day (24-hour format)</label>
|
||||
<input type="time" class="border rounded px-3 py-2 w-full" id="schedule-time" name="schedule_time" value="{{ schedule_time or '03:00' }}">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-medium mb-1" for="frequency">Frequency</label>
|
||||
<select class="border rounded px-3 py-2 w-full" id="frequency" name="frequency">
|
||||
<option value="hourly" {% if schedule_frequency == 'hourly' %}selected{% endif %}>Hourly</option>
|
||||
<option value="daily" {% if schedule_frequency == 'daily' or not schedule_frequency %}selected{% endif %}>Daily</option>
|
||||
<option value="weekly" {% if schedule_frequency == 'weekly' %}selected{% endif %}>Weekly</option>
|
||||
<option value="monthly" {% if schedule_frequency == 'monthly' %}selected{% endif %}>Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block font-medium mb-1" for="retention-days">Retention Policy (days)</label>
|
||||
<input type="number" class="border rounded px-3 py-2 w-full" id="retention-days" name="retention_days" value="{{ retention_days }}" min="0" max="365" onchange="syncRetentionDays(this.value)">
|
||||
<p class="text-sm text-gray-500 mt-1">Enter 0 to keep all backups indefinitely</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
<button type="submit" class="bg-teal-500 text-white px-4 py-2 rounded hover:bg-teal-600">
|
||||
<i class="fas fa-save mr-2"></i> Save Schedule
|
||||
</button>
|
||||
|
||||
<button type="button" onclick="toggleSchedulerForm()" class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Configuration Suggestion Button (replacing Docker Compose Labels button) -->
|
||||
<div class="mt-4 pt-4 border-t border-gray-200">
|
||||
<button type="button" onclick="openConfigModal()" class="mt-2 bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600">
|
||||
<i class="fas fa-file-code mr-2"></i> View Configuration Suggestion
|
||||
</button>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
View suggested configuration for scheduled backups
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Backup Form (Hidden by default) -->
|
||||
<div id="create-backup-form" class="mb-8 p-4 border border-navy-200 rounded-lg {% if not show_create_form %}hidden{% endif %}">
|
||||
<h3 class="text-lg font-semibold mb-4 text-navy-700">Create Custom Backup</h3>
|
||||
|
||||
<form method="POST" action="{{ url_for('users.create_backup') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="backup_name">Backup Name (Optional)</label>
|
||||
<input type="text" id="backup_name" name="backup_name"
|
||||
class="border rounded px-3 py-2 w-full"
|
||||
placeholder="e.g., pre_upgrade_backup">
|
||||
<p class="text-sm text-gray-500 mt-1">Leave blank for automatic timestamp-based name</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-4">
|
||||
<input type="checkbox" id="include_mp3s" name="include_mp3s" value="true"
|
||||
checked
|
||||
class="w-4 h-4 text-teal-600 mr-2">
|
||||
<label for="include_mp3s" class="font-medium">Include MP3 Files</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-4">
|
||||
<input type="checkbox" id="include_config" name="include_config" value="true"
|
||||
checked
|
||||
class="w-4 h-4 text-teal-600 mr-2">
|
||||
<label for="include_config" class="font-medium">Include Configuration Files</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="submit" class="bg-teal-500 text-white px-4 py-2 rounded hover:bg-teal-600">
|
||||
<i class="fas fa-save mr-2"></i> Create Backup
|
||||
</button>
|
||||
<button type="button" onclick="toggleCreateForm()" class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Suggestion Modal (replacing Docker Compose Labels Modal) -->
|
||||
<div id="config-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center hidden overflow-y-auto p-4">
|
||||
<div class="bg-white rounded-lg w-full max-w-3xl max-h-[90vh] overflow-y-auto p-6">
|
||||
<div class="flex justify-between items-center mb-4 sticky top-0 bg-white pb-2">
|
||||
<h3 class="text-xl font-semibold text-navy-700">Backup Configuration Suggestion</h3>
|
||||
<button onclick="closeConfigModal()" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-gray-600 mb-2">
|
||||
Here's a suggested configuration for scheduled backups:
|
||||
</p>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
{% if config_suggestion %}
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-navy-700 mb-2">Docker Compose Labels</h4>
|
||||
<pre class="text-sm font-mono overflow-x-auto whitespace-pre-wrap">{{ config_suggestion.docker_compose_suggestion }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium text-navy-700 mb-2">Ofelia Configuration (Alternative)</h4>
|
||||
<pre class="text-sm font-mono overflow-x-auto whitespace-pre-wrap">{{ config_suggestion.ofelia_ini_suggestion }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-4">
|
||||
<h4 class="font-medium text-navy-700 mb-2">Instructions</h4>
|
||||
<pre class="text-sm font-mono overflow-x-auto whitespace-pre-wrap">{{ config_suggestion.instructions }}</pre>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">Configuration suggestion not available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-400"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700">
|
||||
This is a configuration suggestion only. You'll need to implement it manually based on your system setup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button onclick="copyConfigToClipboard()" class="bg-teal-500 text-white px-4 py-2 rounded hover:bg-teal-600 mr-2">
|
||||
<i class="fas fa-copy mr-2"></i> Copy to Clipboard
|
||||
</button>
|
||||
<button onclick="closeConfigModal()" class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Modal -->
|
||||
<div id="notification-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white rounded-lg max-w-md w-full p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-semibold" id="notification-title">Notification</h3>
|
||||
<button onclick="closeNotificationModal()" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p id="notification-message" class="text-gray-600"></p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button onclick="closeNotificationModal()" class="bg-teal-500 text-white px-4 py-2 rounded hover:bg-teal-600">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup List -->
|
||||
<div id="backup-list" class="mb-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-semibold text-navy-700">Existing Backups</h3>
|
||||
|
||||
<!-- Upload Backup Button -->
|
||||
<button type="button" onclick="toggleUploadForm()" class="bg-navy-600 text-white px-4 py-2 rounded hover:bg-navy-700">
|
||||
<i class="fas fa-upload mr-2"></i> Upload Backup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Upload Backup Form (Hidden by default) -->
|
||||
<div id="upload-backup-form" class="mb-4 p-4 border border-navy-200 rounded-lg hidden">
|
||||
<h4 class="text-lg font-semibold mb-3 text-navy-700">Upload Backup File</h4>
|
||||
|
||||
<form method="POST" action="{{ url_for('users.upload_backup') }}" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="backup_file">Select Backup ZIP File</label>
|
||||
<input type="file" id="backup_file" name="backup_file"
|
||||
accept=".zip"
|
||||
class="border rounded px-3 py-2 w-full">
|
||||
<p class="text-sm text-gray-500 mt-1">Only .zip backup files are supported</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="submit" class="bg-teal-500 text-white px-4 py-2 rounded hover:bg-teal-600">
|
||||
<i class="fas fa-upload mr-2"></i> Upload Backup
|
||||
</button>
|
||||
<button type="button" onclick="toggleUploadForm()" class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Retention Policy (add after Upload Backup Form) -->
|
||||
<div id="retention-policy-section" class="mb-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-semibold text-navy-700">Backup Retention Policy</h3>
|
||||
|
||||
<!-- Toggle Retention Policy Form Button -->
|
||||
<button type="button" onclick="toggleRetentionForm()" class="bg-navy-600 text-white px-4 py-2 rounded hover:bg-navy-700">
|
||||
<i class="fas fa-calendar-alt mr-2"></i> Configure Retention
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Current Retention Policy Status -->
|
||||
<div class="mb-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="h-4 w-4 rounded-full {% if retention_days > 0 %}bg-green-500{% else %}bg-gray-400{% endif %} mr-2"></div>
|
||||
<h4 class="font-medium">Retention Policy Status</h4>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
{% if retention_days > 0 %}
|
||||
Currently keeping backups for {{ retention_days }} days. Older backups will be automatically deleted.
|
||||
{% else %}
|
||||
No retention policy is currently applied. All backups will be kept indefinitely.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Retention Policy Form (Hidden by default) -->
|
||||
<div id="retention-policy-form" class="mb-4 p-4 border border-navy-200 rounded-lg hidden">
|
||||
<h4 class="text-lg font-semibold mb-3 text-navy-700">Configure Backup Retention</h4>
|
||||
|
||||
<form method="POST" action="{{ url_for('users.apply_retention_policy') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="retention_days_policy">Keep Backups For</label>
|
||||
<div class="flex items-center">
|
||||
<input type="number" id="retention_days_policy" name="retention_days"
|
||||
min="0" max="365" value="{{ retention_days }}"
|
||||
class="border rounded px-3 py-2 w-24 mr-2" onchange="syncRetentionDays(this.value)">
|
||||
<span>days</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1">Enter 0 to keep all backups indefinitely</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="submit" class="bg-teal-500 text-white px-4 py-2 rounded hover:bg-teal-600">
|
||||
<i class="fas fa-save mr-2"></i> Save Policy
|
||||
</button>
|
||||
<button type="submit" name="apply_now" value="true" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||
onclick="return confirm('Are you sure you want to apply the retention policy now? This will delete backups older than the specified period.')">
|
||||
<i class="fas fa-trash-alt mr-2"></i> Apply Now
|
||||
</button>
|
||||
<button type="button" onclick="toggleRetentionForm()" class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if backups %}
|
||||
<div class="overflow-x-auto border rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Backup Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Size</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for backup in backups %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="font-medium text-gray-900">{{ backup.backup_name }}</div>
|
||||
<div class="text-xs text-gray-500">{{ backup.file_name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if backup.timestamp %}
|
||||
{{ backup.timestamp|timestamp_to_datetime|format_datetime('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
v{{ backup.version }}
|
||||
{% if backup.release_name %}
|
||||
<div class="text-xs text-gray-500">{{ backup.release_name }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ (backup.file_size / 1024 / 1024)|round(2) }} MB
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ url_for('users.download_backup', filename=backup.file_name) }}" class="text-teal-600 hover:text-teal-900" title="Download">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
|
||||
<form method="POST" action="{{ url_for('users.verify_backup', filename=backup.file_name) }}" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="text-blue-600 hover:text-blue-900" title="Verify">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ url_for('users.restore_backup', filename=backup.file_name) }}" class="inline"
|
||||
onsubmit="return confirm('Are you sure you want to restore this backup? This will overwrite current data.')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="text-orange-600 hover:text-orange-900" title="Restore">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ url_for('users.delete_backup', filename=backup.file_name) }}" class="inline"
|
||||
onsubmit="return confirm('Are you sure you want to delete this backup?')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="text-red-600 hover:text-red-900" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-gray-50 p-4 rounded-lg text-center">
|
||||
<p class="text-gray-500">No backups found.</p>
|
||||
<button onclick="toggleCreateForm()" class="mt-2 text-teal-600 hover:text-teal-800">
|
||||
<i class="fas fa-plus-circle mr-1"></i> Create your first backup
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- System Health Status -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">System Health</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="border rounded-lg p-4 bg-white">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="h-4 w-4 rounded-full bg-green-500 mr-2"></div>
|
||||
<h4 class="font-medium">Database</h4>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">Database is operational and accessible.</p>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-lg p-4 bg-white">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="h-4 w-4 rounded-full bg-green-500 mr-2"></div>
|
||||
<h4 class="font-medium">File Storage</h4>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">File storage is available and writable.</p>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-lg p-4 bg-white">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="h-4 w-4 rounded-full bg-green-500 mr-2"></div>
|
||||
<h4 class="font-medium">Configuration</h4>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">System configuration is valid.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back to Admin -->
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ url_for('users.system_settings') }}" class="bg-navy-600 text-white px-6 py-2 rounded hover:bg-navy-700">
|
||||
<i class="fas fa-cog mr-2"></i> Back to System Settings
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('core.index') }}" class="bg-gray-300 text-gray-700 px-6 py-2 rounded hover:bg-gray-400">
|
||||
Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function toggleCreateForm() {
|
||||
const form = document.getElementById('create-backup-form');
|
||||
form.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function toggleUploadForm() {
|
||||
const form = document.getElementById('upload-backup-form');
|
||||
form.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function toggleRetentionForm() {
|
||||
const form = document.getElementById('retention-policy-form');
|
||||
form.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function toggleSchedulerForm() {
|
||||
const form = document.getElementById('scheduler-form');
|
||||
form.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function openConfigModal() {
|
||||
const modal = document.getElementById('config-modal');
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeConfigModal() {
|
||||
const modal = document.getElementById('config-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function copyConfigToClipboard() {
|
||||
const content = document.querySelector('#config-modal pre').textContent;
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
showNotificationModal('Success', 'Configuration suggestion copied to clipboard!');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
showNotificationModal('Error', 'Failed to copy to clipboard. Please select and copy manually.');
|
||||
});
|
||||
}
|
||||
|
||||
function closeNotificationModal() {
|
||||
const modal = document.getElementById('notification-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showNotificationModal(title, message) {
|
||||
const modal = document.getElementById('notification-modal');
|
||||
document.getElementById('notification-title').textContent = title;
|
||||
document.getElementById('notification-message').textContent = message;
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Function to sync retention days between both forms
|
||||
function syncRetentionDays(value) {
|
||||
// Update both retention days inputs with the same value
|
||||
document.getElementById('retention-days').value = value;
|
||||
document.getElementById('retention_days_policy').value = value;
|
||||
}
|
||||
|
||||
// Show notification if passed from backend
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
{% if notification %}
|
||||
showNotificationModal('Notification', '{{ notification.message }}');
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,95 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Spotify Token Wizard{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<div class="flex items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-navy-800 flex-grow">Spotify Refresh Token Wizard</h2>
|
||||
<img src="https://storage.googleapis.com/pr-newsroom-wp/1/2018/11/Spotify_Logo_RGB_Green.png" alt="Spotify Logo" class="h-8">
|
||||
</div>
|
||||
|
||||
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<h3 class="text-lg font-medium mb-2">What is this for?</h3>
|
||||
<p class="mb-2">This wizard helps you generate a Spotify refresh token for the system account (fallback account). This token will be used when:</p>
|
||||
<ul class="list-disc pl-6 mb-2">
|
||||
<li>A user doesn't have their own Spotify account connected</li>
|
||||
<li>The system needs to perform Spotify API operations in the background</li>
|
||||
<li>For shared/global Spotify functionality</li>
|
||||
</ul>
|
||||
<p class="text-sm text-gray-600">The refresh token doesn't expire, making it ideal for long-term system use.</p>
|
||||
</div>
|
||||
|
||||
{% if has_token %}
|
||||
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div class="flex items-center text-green-700 mb-2">
|
||||
<i class="fas fa-check-circle mr-2"></i>
|
||||
<span class="font-medium">Fallback token is configured</span>
|
||||
</div>
|
||||
<p>A Spotify refresh token is already configured for the system account. You can replace it if needed.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if has_credentials %}
|
||||
<div class="mb-8 border-b pb-4">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Option 1: Automated Setup (Recommended)</h3>
|
||||
<p class="mb-4">This will guide you through the Spotify OAuth flow to generate a refresh token automatically.</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="action" value="start_auth">
|
||||
|
||||
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white py-2 px-6 rounded-md flex items-center">
|
||||
<i class="fab fa-spotify mr-2"></i> Start Spotify Authorization
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-3 text-sm text-gray-600">
|
||||
<p>You'll be redirected to Spotify to authorize access, then brought back to this page when complete.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Option 2: Manual Entry</h3>
|
||||
<p class="mb-4">If you already have a Spotify refresh token (obtained elsewhere), you can enter it directly:</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="action" value="manual_save">
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="refresh_token" class="block font-medium mb-1">Refresh Token</label>
|
||||
<input type="password" id="refresh_token" name="refresh_token"
|
||||
class="border rounded px-3 py-2 w-full" placeholder="Enter your Spotify refresh token">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-navy-600 hover:bg-navy-700 text-white py-2 px-6 rounded-md">
|
||||
Save Token
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div class="flex items-center text-yellow-700 mb-2">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
<span class="font-medium">Spotify API credentials not configured</span>
|
||||
</div>
|
||||
<p class="mb-3">Your Spotify API credentials are missing or incomplete. The following values need to be set in your environment configuration:</p>
|
||||
<ul class="list-disc pl-6">
|
||||
<li>SPOTIFY_CLIENT_ID</li>
|
||||
<li>SPOTIFY_CLIENT_SECRET</li>
|
||||
<li>SPOTIFY_REDIRECT_URI</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-8 flex justify-between">
|
||||
<a href="{{ url_for('users.system_settings') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-800 py-2 px-6 rounded-md">
|
||||
Back to Settings
|
||||
</a>
|
||||
|
||||
<a href="https://developer.spotify.com/documentation/web-api/concepts/access-token" target="_blank" class="text-navy-600 hover:underline flex items-center">
|
||||
<span>Learn about Spotify authentication</span>
|
||||
<i class="fas fa-external-link-alt ml-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,219 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}System Health{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-6xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-navy-800">System Health Status</h2>
|
||||
<div class="text-sm text-navy-600">{{ version_info.version }} - {{ version_info.release_name }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Health Summary Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="bg-{{ database_status.color }}-50 border border-{{ database_status.color }}-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="h-4 w-4 rounded-full bg-{{ database_status.color }}-500 mr-2"></div>
|
||||
<h3 class="font-semibold text-{{ database_status.color }}-700">Database</h3>
|
||||
</div>
|
||||
<p class="text-sm text-{{ database_status.color }}-600">{{ database_status.message }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-{{ storage_status.color }}-50 border border-{{ storage_status.color }}-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="h-4 w-4 rounded-full bg-{{ storage_status.color }}-500 mr-2"></div>
|
||||
<h3 class="font-semibold text-{{ storage_status.color }}-700">Storage</h3>
|
||||
</div>
|
||||
<p class="text-sm text-{{ storage_status.color }}-600">{{ storage_status.message }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-{{ api_status.color }}-50 border border-{{ api_status.color }}-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="h-4 w-4 rounded-full bg-{{ api_status.color }}-500 mr-2"></div>
|
||||
<h3 class="font-semibold text-{{ api_status.color }}-700">API Services</h3>
|
||||
</div>
|
||||
<p class="text-sm text-{{ api_status.color }}-600">{{ api_status.message }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-{{ memory_status.color }}-50 border border-{{ memory_status.color }}-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="h-4 w-4 rounded-full bg-{{ memory_status.color }}-500 mr-2"></div>
|
||||
<h3 class="font-semibold text-{{ memory_status.color }}-700">Memory</h3>
|
||||
</div>
|
||||
<p class="text-sm text-{{ memory_status.color }}-600">{{ memory_status.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Details -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Database Information</h3>
|
||||
<div class="overflow-x-auto border rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Metric</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
Total Songs
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ database_stats.song_count|default('0') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
Total Rounds
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ database_stats.round_count|default('0') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
Total Users
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ database_stats.user_count|default('0') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
Database File Size
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ database_stats.file_size|default('Unknown') }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
Last Backup
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ database_stats.last_backup|default('Never') }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Details -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Storage Information</h3>
|
||||
<div class="overflow-x-auto border rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Directory</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Files</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Size</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for dir in storage_stats %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ dir.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ dir.file_count }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ dir.size }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{% if dir.writable %}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
Writable
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||
Not Writable
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Status -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">External Services</h3>
|
||||
<div class="overflow-x-auto border rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for service in service_stats %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ service.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{% if service.status == 'ok' %}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
Available
|
||||
</span>
|
||||
{% elif service.status == 'warning' %}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||
Warning
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||
Unavailable
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
{{ service.message }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Information -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Version Information</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm"><strong>Application Version:</strong> {{ version_info.version }}</p>
|
||||
<p class="text-sm"><strong>Release Name:</strong> {{ version_info.release_name }}</p>
|
||||
<p class="text-sm"><strong>Release Date:</strong> {{ version_info.release_date }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm"><strong>Python Version:</strong> {{ system_info.python_version }}</p>
|
||||
<p class="text-sm"><strong>Platform:</strong> {{ system_info.platform }}</p>
|
||||
<p class="text-sm"><strong>Flask Version:</strong> {{ system_info.flask_version }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ url_for('users.backup_manager') }}" class="bg-teal-500 text-white px-6 py-2 rounded hover:bg-teal-600">
|
||||
<i class="fas fa-database mr-2"></i> Go to Backup Manager
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('users.system_settings') }}" class="bg-navy-600 text-white px-6 py-2 rounded hover:bg-navy-700">
|
||||
<i class="fas fa-cog mr-2"></i> Back to System Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,257 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}System Settings{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-6 text-navy-800">System Settings</h2>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<!-- TTS Settings Section -->
|
||||
<div class="mb-8 border-b pb-6">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Text-to-Speech Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="default_tts_service">Default TTS Service</label>
|
||||
<select name="default_tts_service" id="default_tts_service" class="border rounded px-3 py-2 w-full">
|
||||
<option value="">(None)</option>
|
||||
{% for service in tts_services %}
|
||||
<option value="{{ service.id }}" {% if settings.get('default_tts_service') == service.id %}selected{% endif %}>
|
||||
{{ service.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-sm text-gray-500 mt-1">The default text-to-speech service to use for audio generation</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="default_tts_voice">Default TTS Voice</label>
|
||||
<select name="default_tts_voice" id="default_tts_voice" class="border rounded px-3 py-2 w-full">
|
||||
<option value="">(None)</option>
|
||||
|
||||
{% for service in tts_services %}
|
||||
<optgroup label="{{ service.name }}">
|
||||
{% for voice in service.voices %}
|
||||
<option value="{{ voice.id }}" {% if settings.get('default_tts_voice') == voice.id %}selected{% endif %}>
|
||||
{{ voice.name }} {% if voice.gender or voice.language %}({{ voice.gender }}{% if voice.language and voice.gender %}, {{ voice.language }}{% elif voice.language %}{{ voice.language }}{% endif %}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-sm text-gray-500 mt-1">Default voice ID for the selected TTS service</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="default_tts_model">Default TTS Model</label>
|
||||
<select name="default_tts_model" id="default_tts_model" class="border rounded px-3 py-2 w-full">
|
||||
<option value="">(None)</option>
|
||||
|
||||
{% for service in tts_services %}
|
||||
{% if service.models %}
|
||||
<optgroup label="{{ service.name }} Models">
|
||||
{% for model in service.models %}
|
||||
<option value="{{ model.id }}" {% if settings.get('default_tts_model') == model.id %}selected{% endif %}>
|
||||
{{ model.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-sm text-gray-500 mt-1">Default model for OpenAI or ElevenLabs TTS</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spotify Integration Settings -->
|
||||
<div class="mb-8 border-b pb-6">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Spotify Integration</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="fallback_spotify_refresh_token">Fallback Spotify Refresh Token</label>
|
||||
<div class="flex">
|
||||
<input type="password" name="fallback_spotify_refresh_token" id="fallback_spotify_refresh_token"
|
||||
class="border rounded-l px-3 py-2 flex-grow"
|
||||
value="{{ settings.get('fallback_spotify_refresh_token', '') }}">
|
||||
<button type="button" onclick="toggleTokenVisibility()"
|
||||
class="bg-gray-200 px-3 py-2 rounded-r border-t border-r border-b">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1">Service account refresh token used when users don't have Spotify access</p>
|
||||
<div class="mt-2">
|
||||
<a href="{{ url_for('users.spotify_link') }}" class="bg-navy-600 text-white px-4 py-2 rounded inline-flex items-center">
|
||||
<i class="fab fa-spotify mr-2"></i> Connect Spotify Account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="spotify_region">Default Spotify Region</label>
|
||||
<select name="spotify_region" id="spotify_region" class="border rounded px-3 py-2 w-full">
|
||||
<option value="">(None)</option>
|
||||
{% for region in spotify_regions %}
|
||||
<option value="{{ region.code }}" {% if settings.get('spotify_region') == region.code %}selected{% endif %}>
|
||||
{{ region.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="text-sm text-gray-500 mt-1">Default region for Spotify API searches and charts</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authentication Settings Section -->
|
||||
<div class="mb-8 border-b pb-6">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Authentication Settings</h3>
|
||||
|
||||
<div class="flex items-center mb-4">
|
||||
<input type="checkbox" id="allow_signups" name="allow_signups" value="true"
|
||||
{% if settings.get('allow_signups', 'true') == 'true' %}checked{% endif %}
|
||||
class="w-4 h-4 text-blue-600 mr-2">
|
||||
<label for="allow_signups" class="font-medium">Allow New User Registrations</label>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
When disabled, new users cannot create accounts. Existing users can still log in, and OAuth login will still work for existing accounts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Backup & System Health Section -->
|
||||
<div class="mb-8 border-b pb-6">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Backup & System Health</h3>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<a href="{{ url_for('users.backup_manager') }}" class="flex-1 bg-teal-500 hover:bg-teal-600 text-white py-3 px-4 rounded flex items-center justify-center">
|
||||
<i class="fas fa-database mr-2"></i> Backup Manager
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('users.system_health') }}" class="flex-1 bg-navy-600 hover:bg-navy-700 text-white py-3 px-4 rounded flex items-center justify-center">
|
||||
<i class="fas fa-heartbeat mr-2"></i> System Health
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 mt-2">
|
||||
Manage system backups, restore from previous backups, and monitor system health.
|
||||
</p>
|
||||
|
||||
<div class="mt-4 bg-blue-50 border border-blue-200 rounded-md p-3">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-info-circle text-blue-500"></i>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700">
|
||||
Regular backups are recommended to prevent data loss. The backup system will save your database, MP3 files, and system configuration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Settings -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold mb-4 text-navy-700">Additional Settings</h3>
|
||||
|
||||
{% for key, label in editable_settings %}
|
||||
{% if key not in ['default_tts_service', 'default_tts_voice', 'default_tts_model', 'fallback_spotify_refresh_token', 'spotify_region', 'allow_signups'] %}
|
||||
<div class="mb-4">
|
||||
<label class="block font-medium mb-1" for="{{ key }}">{{ label }}</label>
|
||||
{% if key == 'enable_public_rounds' %}
|
||||
<select name="{{ key }}" id="{{ key }}" class="border rounded px-3 py-2 w-full">
|
||||
<option value="true" {% if settings.get(key) == 'true' %}selected{% endif %}>Enabled</option>
|
||||
<option value="false" {% if settings.get(key) != 'true' %}selected{% endif %}>Disabled</option>
|
||||
</select>
|
||||
{% else %}
|
||||
<input type="text" name="{{ key }}" id="{{ key }}" class="border rounded px-3 py-2 w-full"
|
||||
value="{{ settings.get(key, '') }}">
|
||||
{% endif %}
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{% if key == 'max_songs_per_round' %}
|
||||
Maximum number of songs that can be included in a music round
|
||||
{% elif key == 'enable_public_rounds' %}
|
||||
Allow users to make their music rounds publicly accessible
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<button type="submit" class="bg-teal-500 text-white px-6 py-2 rounded hover:bg-teal-600">
|
||||
<i class="fas fa-save mr-2"></i> Save Settings
|
||||
</button>
|
||||
<a href="{{ url_for('core.index') }}" class="bg-gray-300 text-gray-700 px-6 py-2 rounded hover:bg-gray-400">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function toggleTokenVisibility() {
|
||||
const tokenField = document.getElementById('fallback_spotify_refresh_token');
|
||||
if (tokenField.type === 'password') {
|
||||
tokenField.type = 'text';
|
||||
} else {
|
||||
tokenField.type = 'password';
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic voice selection based on service
|
||||
document.getElementById('default_tts_service').addEventListener('change', function() {
|
||||
const selectedService = this.value;
|
||||
const voiceSelect = document.getElementById('default_tts_voice');
|
||||
const modelSelect = document.getElementById('default_tts_model');
|
||||
|
||||
// Hide all options first
|
||||
Array.from(voiceSelect.options).forEach(option => {
|
||||
option.style.display = 'none';
|
||||
});
|
||||
|
||||
Array.from(modelSelect.options).forEach(option => {
|
||||
option.style.display = 'none';
|
||||
});
|
||||
|
||||
// Show only options for the selected service
|
||||
Array.from(voiceSelect.options).forEach(option => {
|
||||
if (option.value === '' || option.parentNode.label.includes(selectedService)) {
|
||||
option.style.display = '';
|
||||
}
|
||||
});
|
||||
|
||||
Array.from(modelSelect.options).forEach(option => {
|
||||
if (option.value === '' || option.parentNode.label.includes(selectedService)) {
|
||||
option.style.display = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Reset to empty if current selection is not valid for the service
|
||||
let validVoice = false;
|
||||
Array.from(voiceSelect.options).forEach(option => {
|
||||
if (option.selected && option.style.display !== 'none') {
|
||||
validVoice = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!validVoice) {
|
||||
voiceSelect.value = '';
|
||||
}
|
||||
|
||||
let validModel = false;
|
||||
Array.from(modelSelect.options).forEach(option => {
|
||||
if (option.selected && option.style.display !== 'none') {
|
||||
validModel = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!validModel) {
|
||||
modelSelect.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger the change event on page load to set up initial state
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('default_tts_service').dispatchEvent(new Event('change'));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user