Add dashboard auto-refresh, status filters, rate limiting, database backup/restore
This commit is contained in:
parent
f32efc42c5
commit
5ebce2baff
5 changed files with 138 additions and 29 deletions
67
app.py
67
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/<int:rid>', 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,
|
||||
)
|
||||
|
||||
|
|
|
|||
Reference in a new issue