Add configurable max revisions limit with customer counter and operator override

This commit is contained in:
Troll (Hermes Agent) 2026-08-01 21:36:23 +00:00
parent 5b6012aa3d
commit 506d07179a
5 changed files with 99 additions and 22 deletions

72
app.py
View file

@ -33,6 +33,8 @@ import ssl
from email.message import EmailMessage from email.message import EmailMessage
from pathlib import Path from pathlib import Path
import json
# Flask and related imports # 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
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@ -202,7 +204,19 @@ def play(token):
req = get_request_by_token(token) req = get_request_by_token(token)
if not req: if not req:
abort(404) abort(404)
return render_template('player.html', req=req)
# Load runtime max revisions setting.
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
runtime_settings = {}
if cfg_path.exists():
try:
runtime_settings = json.loads(cfg_path.read_text())
except (json.JSONDecodeError, OSError):
runtime_settings = {}
max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
revisions_left = max(0, max_revisions - (req.get('revision_count') or 0))
return render_template('player.html', req=req, revisions_left=revisions_left)
@app.route('/play/<token>/approve', methods=['POST']) @app.route('/play/<token>/approve', methods=['POST'])
@ -236,10 +250,26 @@ def revise(token):
so the operator sees it in the dashboard queue. so the operator sees it in the dashboard queue.
""" """
note = request.form.get('revision_note', '').strip() note = request.form.get('revision_note', '').strip()
# Increment revision counter and archive current files before new versions are uploaded.
req = get_request_by_token(token) req = get_request_by_token(token)
if req: if not req:
new_count = (req.get('revision_count') or 0) + 1 abort(404)
# Enforce max revisions limit for customer-submitted revisions.
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
runtime_settings = {}
if cfg_path.exists():
try:
runtime_settings = json.loads(cfg_path.read_text())
except (json.JSONDecodeError, OSError):
runtime_settings = {}
max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
current_count = req.get('revision_count') or 0
if current_count >= max_revisions:
flash('Revision limit reached. Please speak to the booth operator if you need further changes.', 'error')
return redirect(url_for('play', token=token))
# Increment revision counter and archive current files before new versions are uploaded.
new_count = current_count + 1
upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(req['id']) upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(req['id'])
if upload_dir.exists(): if upload_dir.exists():
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')): for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
@ -249,15 +279,12 @@ def revise(token):
archived = upload_dir / f"Rev{new_count}-{old.name}" archived = upload_dir / f"Rev{new_count}-{old.name}"
try: try:
old.rename(archived) old.rename(archived)
# Keep path pointing to the archived file; operator will upload new files.
req[field] = str(archived) req[field] = str(archived)
except OSError: except OSError:
pass pass
update_request(req['id'], revision_note=note, status='revisions_requested', update_request(req['id'], revision_note=note, status='revisions_requested',
song_a_path=req.get('song_a_path'), song_b_path=req.get('song_b_path'), song_a_path=req.get('song_a_path'), song_b_path=req.get('song_b_path'),
customer_approved='none', revision_count=new_count) customer_approved='none', revision_count=new_count)
else:
abort(404)
# NOTE: No operator email is sent; the dashboard is the single queue. # NOTE: No operator email is sent; the dashboard is the single queue.
flash('Your feedback has been saved. We will regenerate and update you.', 'success') flash('Your feedback has been saved. We will regenerate and update you.', 'success')
@ -479,6 +506,16 @@ def admin_settings():
for req in all_requests: for req in all_requests:
status_counts[req['status']] = status_counts.get(req['status'], 0) + 1 status_counts[req['status']] = status_counts.get(req['status'], 0) + 1
# Load persistent runtime settings (max_revisions overrides env var if set).
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
runtime_settings = {}
if cfg_path.exists():
try:
runtime_settings = json.loads(cfg_path.read_text())
except (json.JSONDecodeError, OSError):
runtime_settings = {}
current_max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
# Compute upload folder stats. # Compute upload folder stats.
total_upload_size = 0 total_upload_size = 0
upload_file_count = 0 upload_file_count = 0
@ -555,6 +592,24 @@ def admin_settings():
flash('System reset complete. All orders and files have been cleared.', 'success') flash('System reset complete. All orders and files have been cleared.', 'success')
return redirect(url_for('admin_settings')) return redirect(url_for('admin_settings'))
elif action == 'save_max_revisions':
# Update the MAX_REVISIONS config from the settings form.
try:
val = int(request.form.get('max_revisions', '2').strip())
if val < 0:
raise ValueError
# Store in a simple config file so it persists across restarts.
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
data = {}
if cfg_path.exists():
data = json.loads(cfg_path.read_text())
data['max_revisions'] = val
cfg_path.write_text(json.dumps(data))
flash(f'Maximum revisions set to {val}.', 'success')
except ValueError:
flash('Invalid revision limit. Please enter a non-negative number.', 'error')
return redirect(url_for('admin_settings'))
return render_template( return render_template(
'admin/settings.html', 'admin/settings.html',
health=health, health=health,
@ -566,7 +621,8 @@ def admin_settings():
upload_dir_count=request_dir_count, upload_dir_count=request_dir_count,
upload_size=format_bytes(total_upload_size), upload_size=format_bytes(total_upload_size),
db_path=str(db_path), db_path=str(db_path),
upload_path=str(upload_root) upload_path=str(upload_root),
current_max_revisions=current_max_revisions,
) )

1
booth_settings.json Normal file
View file

@ -0,0 +1 @@
{"max_revisions": 1}

View file

@ -47,6 +47,9 @@ class Config:
# Optional operator alert email. Currently unused because the dashboard is the queue. # Optional operator alert email. Currently unused because the dashboard is the queue.
ADMIN_ALERT_EMAIL = os.environ.get('ADMIN_ALERT_EMAIL', '') ADMIN_ALERT_EMAIL = os.environ.get('ADMIN_ALERT_EMAIL', '')
# Maximum number of revision rounds a customer is allowed to request automatically.
MAX_REVISIONS = int(os.environ.get('MAX_REVISIONS', '2'))
# Public HTTPS URL used in customer emails and QR codes. # Public HTTPS URL used in customer emails and QR codes.
PUBLIC_BASE_URL = os.environ.get('PUBLIC_BASE_URL', 'http://127.0.0.1:5000') PUBLIC_BASE_URL = os.environ.get('PUBLIC_BASE_URL', 'http://127.0.0.1:5000')

View file

@ -194,6 +194,18 @@
<p><strong>Uploads:</strong><br><span class="path">{{ upload_path }}</span></p> <p><strong>Uploads:</strong><br><span class="path">{{ upload_path }}</span></p>
</div> </div>
<!-- Settings controls -->
<div class="section">
<h2>Booth Settings</h2>
<form method="POST">
<input type="hidden" name="action" value="save_max_revisions">
<label for="max_revisions">Maximum customer revisions allowed</label>
<input type="number" id="max_revisions" name="max_revisions" min="0" value="{{ current_max_revisions }}">
<p class="copy-hint">Set to 0 to disable customer-submitted revisions. Operators can still upload new versions manually.</p>
<button type="submit">Save Revision Limit</button>
</form>
</div>
<!-- System reset --> <!-- System reset -->
<div class="danger-zone"> <div class="danger-zone">
<h2>⚠️ Reset System</h2> <h2>⚠️ Reset System</h2>

View file

@ -130,14 +130,19 @@
</div> </div>
</form> </form>
{% if revisions_left > 0 %}
<!-- Revision form: customer asks for changes --> <!-- Revision form: customer asks for changes -->
<form method="POST" action="{{ url_for('revise', token=req.player_token) }}"> <form method="POST" action="{{ url_for('revise', token=req.player_token) }}">
<p style="color:#93c5fd;font-weight:600">Revisions remaining: {{ revisions_left }}</p>
<label for="revision_note">Or ask for changes:</label> <label for="revision_note">Or ask for changes:</label>
<textarea id="revision_note" name="revision_note" placeholder="e.g. make the chorus louder, swap a lyric..."></textarea> <textarea id="revision_note" name="revision_note" placeholder="e.g. make the chorus louder, swap a lyric..."></textarea>
<div class="actions"> <div class="actions">
<button type="submit" class="revision">Request Changes</button> <button type="submit" class="revision">Request Changes</button>
</div> </div>
</form> </form>
{% else %}
<p style="margin-top:1rem;color:#f87171;font-weight:600">No revisions remaining. Please speak to the booth operator if you need further changes.</p>
{% endif %}
{% endif %} {% endif %}
</div> </div>