diff --git a/app.py b/app.py index 05a6be4..270dd59 100644 --- a/app.py +++ b/app.py @@ -30,13 +30,16 @@ import os import shutil import smtplib import ssl +import time from email.message import EmailMessage from pathlib import Path import json # Flask and related imports -from flask import Flask, request, render_template, redirect, url_for, flash, session, send_from_directory, abort, current_app +from flask import Flask, request, render_template, redirect, url_for, flash, session, send_from_directory, abort, current_app, send_file +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address from werkzeug.utils import secure_filename # Project imports @@ -56,6 +59,9 @@ from mutagen.easyid3 import EasyID3 app = Flask(__name__) app.config.from_object(Config) +# Request rate limiting: by remote IP. Defaults can be overridden via Limiter storage when configured. +limiter = Limiter(get_remote_address, app=app, default_limits=["60 per minute"]) + # Ensure the SQLite connection is closed at the end of each request. app.teardown_appcontext(close_db) @@ -189,6 +195,16 @@ def load_booth_settings(): return {} +def get_refresh_seconds(): + """Return the dashboard auto-refresh interval in seconds (10, 20, or 30).""" + cfg = load_booth_settings() + try: + val = int(cfg.get('refresh_seconds', 10)) + except (ValueError, TypeError): + val = 10 + return val if val in (10, 20, 30) else 10 + + def save_booth_settings(settings): """Persist runtime settings to JSON file.""" cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json' @@ -239,6 +255,7 @@ def index(): @app.route('/request', methods=['GET', 'POST']) +@limiter.limit("5 per minute") def request_form(): """ Public request form. @@ -406,7 +423,7 @@ def admin_dashboard(): return redir status_filter = request.args.get('status') requests = list_requests(status_filter) - return render_template('admin/dashboard.html', requests=requests, statuses=STATUS_LABELS, current_status=status_filter) + return render_template('admin/dashboard.html', requests=requests, statuses=STATUS_LABELS, current_status=status_filter, refresh_seconds=get_refresh_seconds()) @app.route('/admin/request/', methods=['GET', 'POST']) @@ -671,12 +688,55 @@ def admin_settings(): cfg['artist'] = request.form.get('artist', '').strip() or None cfg['album'] = request.form.get('album', '').strip() or None cfg['year'] = request.form.get('year', '').strip() or None - cfg['genre'] = request.form.get('genre', '').strip() or None cfg['comment'] = request.form.get('comment', '').strip() or None save_booth_settings(cfg) flash('MP3 metadata defaults saved.', 'success') return redirect(url_for('admin_settings')) + elif action == 'save_refresh': + # Update dashboard auto-refresh interval. + val = request.form.get('refresh_seconds', '10').strip() + if val not in ('0', '10', '20', '30'): + val = '10' + cfg = load_booth_settings() + cfg['refresh_seconds'] = int(val) + save_booth_settings(cfg) + flash(f'Dashboard auto-refresh set to {val} seconds.', 'success') + return redirect(url_for('admin_settings')) + + elif action == 'download_db': + # Send the SQLite database file as a download. + if db_path.exists(): + return send_file(str(db_path), as_attachment=True, download_name='theme-song-booth.db') + flash('Database file not found.', 'error') + return redirect(url_for('admin_settings')) + + elif action == 'restore_db': + # Replace the current database file with an uploaded SQLite backup. + file_obj = request.files.get('db_backup') + if not file_obj or file_obj.filename == '': + flash('No database backup file selected.', 'error') + return redirect(url_for('admin_settings')) + backup_path = db_path.with_suffix('.backup-restore') + try: + # Stream uploaded file directly to disk to avoid memory issues with large DBs. + file_obj.save(backup_path) + # Quick sanity check: try to open as SQLite and query sqlite_master. + import sqlite3 + conn = sqlite3.connect(str(backup_path)) + conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + conn.close() + # Replace old database with backup. + old_backup = db_path.with_suffix('.backup-' + str(int(time.time()))) + db_path.rename(old_backup) + backup_path.rename(db_path) + flash('Database restored successfully. Old database kept at ' + old_backup.name, 'success') + except Exception as e: + if backup_path.exists(): + backup_path.unlink() + flash(f'Database restore failed: {e}', 'error') + return redirect(url_for('admin_settings')) + return render_template( 'admin/settings.html', health=health, @@ -690,6 +750,7 @@ def admin_settings(): db_path=str(db_path), upload_path=str(upload_root), current_max_revisions=current_max_revisions, + current_refresh_seconds=runtime_settings.get('refresh_seconds', 10), metadata=runtime_settings, ) diff --git a/booth_settings.json b/booth_settings.json new file mode 100644 index 0000000..a34779b --- /dev/null +++ b/booth_settings.json @@ -0,0 +1,3 @@ +{ + "refresh_seconds": 20 +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cbfdbf8..af86fe5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ gunicorn python-dotenv werkzeug mutagen +flask-limiter diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index 24da000..7b07808 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -4,6 +4,9 @@ Admin Dashboard + {% if refresh_seconds and refresh_seconds > 0 %} + + {% endif %} @@ -123,10 +126,17 @@
- All - {% for key,label in statuses.items() %} - {{ label }} - {% endfor %} + All + Needs Upload + Awaiting Payment + Done + + {% if refresh_seconds and refresh_seconds > 0 %} + Auto-refresh every {{ refresh_seconds }}s + {% else %} + Auto-refresh off + {% endif %} +
diff --git a/templates/admin/settings.html b/templates/admin/settings.html index 78dc7de..7295698 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -218,6 +218,22 @@ + +
+

Dashboard Auto-Refresh

+
+ + + + +
+
+

MP3 Metadata Tags

@@ -246,6 +262,24 @@
+ +
+

Database Backup / Restore

+

Download a copy of the SQLite database before the event. Upload a previous backup to restore it; the current database will be renamed as a timestamped backup.

+ +
+ + +
+ +
+ + + + +
+
+

⚠️ Reset System

@@ -256,7 +290,7 @@
- - - - + + + +