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

100
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,29 +250,42 @@ 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
upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(req['id'])
if upload_dir.exists():
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
path = req.get(field)
if path and Path(path).exists():
old = Path(path)
archived = upload_dir / f"Rev{new_count}-{old.name}"
try:
old.rename(archived)
# Keep path pointing to the archived file; operator will upload new files.
req[field] = str(archived)
except OSError:
pass
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'),
customer_approved='none', revision_count=new_count)
else:
abort(404) 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'])
if upload_dir.exists():
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
path = req.get(field)
if path and Path(path).exists():
old = Path(path)
archived = upload_dir / f"Rev{new_count}-{old.name}"
try:
old.rename(archived)
req[field] = str(archived)
except OSError:
pass
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'),
customer_approved='none', revision_count=new_count)
# 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')
return redirect(url_for('play', token=token)) return redirect(url_for('play', token=token))
@ -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>