feat: editable customer email in admin + email validation on public form

This commit is contained in:
Troll (Hermes Agent) 2026-08-02 16:51:32 +00:00
parent 4af9322c9d
commit bf9f9e09fd
4 changed files with 48 additions and 13 deletions

View file

@ -6,7 +6,7 @@ A Flask web app for a convention booth where visitors request a custom AI-genera
### Customer-facing ### Customer-facing
- **Request form** (`/request`) — visitors enter name, email, hobbies, notable facts, preferred style/genre, vocal gender preference, and extra requests. A branded banner image is shown. - **Request form** (`/request`) — visitors enter name, email, hobbies, notable facts, preferred style/genre, vocal gender preference, and extra requests. A branded banner image is shown. The email field is validated to reduce delivery problems.
- **Confirmation page** (`/thanks/<id>`) — shows the request number after submission. - **Confirmation page** (`/thanks/<id>`) — shows the request number after submission.
- **Private player page** (`/play/<token>`) — customer receives an email with a unique link. They can stream Version A and Version B, pick one (or both), or request a limited number of revisions. - **Private player page** (`/play/<token>`) — customer receives an email with a unique link. They can stream Version A and Version B, pick one (or both), or request a limited number of revisions.
- **Revision workflow** — when a customer asks for changes, the current MP3s are archived and the operator sees the request as "Revisions Requested" in the dashboard. - **Revision workflow** — when a customer asks for changes, the current MP3s are archived and the operator sees the request as "Revisions Requested" in the dashboard.
@ -18,6 +18,7 @@ A Flask web app for a convention booth where visitors request a custom AI-genera
- **Dashboard queue** (`/admin`) — filter by status (All, Pending, Needs Upload, Awaiting Payment, Delivered) and auto-refresh at a configurable interval. - **Dashboard queue** (`/admin`) — filter by status (All, Pending, Needs Upload, Awaiting Payment, Delivered) and auto-refresh at a configurable interval.
- **Per-request detail page** (`/admin/request/<id>`): - **Per-request detail page** (`/admin/request/<id>`):
- Generate and save a Suno prompt from customer info. - Generate and save a Suno prompt from customer info.
- Edit the customer's email address if they mistyped it.
- Upload Version A and Version B MP3s (with automatic ID3 metadata tagging). - Upload Version A and Version B MP3s (with automatic ID3 metadata tagging).
- Send a preview email with a private player link. - Send a preview email with a private player link.
- Mark paid, enter a Square payment reference, and deliver selected MP3 attachments. - Mark paid, enter a Square payment reference, and deliver selected MP3 attachments.
@ -62,7 +63,7 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
| `templates/player.html` | Customer audio player, approval, and revision form. | | `templates/player.html` | Customer audio player, approval, and revision form. |
| `templates/admin/login.html` | Admin login page. | | `templates/admin/login.html` | Admin login page. |
| `templates/admin/dashboard.html` | Operator queue with filters and auto-refresh. | | `templates/admin/dashboard.html` | Operator queue with filters and auto-refresh. |
| `templates/admin/request.html` | Single-request detail / prompt / upload / delivery page. | | `templates/admin/request.html` | Single-request detail / prompt / upload / delivery page. Customer email is editable here. |
| `templates/admin/settings.html` | Maintenance, settings, backup/restore, and reset page. | | `templates/admin/settings.html` | Maintenance, settings, backup/restore, and reset page. |
| `static/Trollgorithm_booth.jpg` | Banner image on the request page. | | `static/Trollgorithm_booth.jpg` | Banner image on the request page. |
| `static/DM-Logo_email.png` | Inline Dionysis Media logo attached to emails. | | `static/DM-Logo_email.png` | Inline Dionysis Media logo attached to emails. |

View file

@ -48,7 +48,7 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
1. Customer fills `/request`. 1. Customer fills `/request`.
2. Open `/admin`, click request row (or filter by status). 2. Open `/admin`, click request row (or filter by status).
3. On `/admin/request/<id>`, click **Copy customer info for Hermes**, paste result to Hermes. 3. On `/admin/request/<id>`, fix the customer's email if needed, then click **Copy customer info for Hermes**, paste result to Hermes.
4. Paste Hermes response (Title/Style/Lyrics format) into the fields and click **Save Prompt**. 4. Paste Hermes response (Title/Style/Lyrics format) into the fields and click **Save Prompt**.
5. Copy Style/Lyrics into Suno Custom Mode, generate two versions. 5. Copy Style/Lyrics into Suno Custom Mode, generate two versions.
6. Upload Version A and B MP3s. 6. Upload Version A and B MP3s.

40
app.py
View file

@ -30,6 +30,7 @@ Admin routes:
# Standard library imports # Standard library imports
import os import os
import re
import shutil import shutil
import smtplib import smtplib
import ssl import ssl
@ -38,7 +39,6 @@ from email.message import EmailMessage
from pathlib import Path from pathlib import Path
import json import json
import time
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
@ -318,14 +318,18 @@ def request_form():
""" """
Public request form. Public request form.
GET -> shows the form with the banner image. GET -> shows the form with the banner image.
POST -> creates a database record, sends a confirmation email, POST -> validates the email, creates a database record, sends a
and redirects to the thanks page. confirmation email, and redirects to the thanks page.
Rate limited to 5 submissions per minute per IP. Rate limited to 5 submissions per minute per IP.
""" """
if request.method == 'POST': if request.method == 'POST':
email = request.form.get('email', '').strip().lower()
if not is_valid_email(email):
flash('Please enter a valid email address.', 'error')
return render_template('request.html'), 400
rid = create_request( rid = create_request(
name=request.form.get('name', '').strip(), name=request.form.get('name', '').strip(),
email=request.form.get('email', '').strip(), email=email,
hobbies=request.form.get('hobbies', '').strip(), hobbies=request.form.get('hobbies', '').strip(),
notable_facts=request.form.get('notable_facts', '').strip(), notable_facts=request.form.get('notable_facts', '').strip(),
style_genre=request.form.get('style_genre', '').strip(), style_genre=request.form.get('style_genre', '').strip(),
@ -491,6 +495,15 @@ def admin_login():
return render_template('admin/login.html') return render_template('admin/login.html')
def is_valid_email(email):
"""Return True if the given string looks like a valid email address."""
if not email:
return False
# Very loose regex: local@domain.tld, no spaces, with a real TLD part.
pattern = r"^[^\s@]+@[^\s@]+\.[^\s@]+$"
return re.match(pattern, email) is not None
@app.route('/admin/logout') @app.route('/admin/logout')
def admin_logout(): def admin_logout():
"""Clear the admin session.""" """Clear the admin session."""
@ -517,9 +530,11 @@ def admin_dashboard():
def admin_request(rid): def admin_request(rid):
""" """
Detail/edit page for a single request. Detail/edit page for a single request.
GET -> render customer info, prompt, upload status, email status, and delivery forms. GET -> render customer info (email editable), prompt, upload status,
POST -> handle one of four actions: email status, and delivery forms.
save_prompt, upload_songs, notify_customer, mark_paid_deliver POST -> handle one of five actions:
update_customer_email, save_prompt, upload_songs,
notify_customer, mark_paid_deliver
Uploaded MP3s are tagged with metadata defaults from /admin/settings. Uploaded MP3s are tagged with metadata defaults from /admin/settings.
""" """
redir = require_admin() redir = require_admin()
@ -549,7 +564,16 @@ def admin_request(rid):
if request.method == 'POST': if request.method == 'POST':
action = request.form.get('action') action = request.form.get('action')
if action == 'save_prompt': if action == 'update_customer_email':
new_email = request.form.get('email', '').strip().lower()
if not is_valid_email(new_email):
flash('Please enter a valid email address.', 'error')
return redirect(url_for('admin_request', rid=rid))
update_request(rid, email=new_email)
flash('Customer email updated.', 'success')
return redirect(url_for('admin_request', rid=rid))
elif action == 'save_prompt':
# Store the generated title/style/lyrics and mark prompt ready. # Store the generated title/style/lyrics and mark prompt ready.
update_request(rid, update_request(rid,
suno_title=request.form.get('suno_title', '').strip(), suno_title=request.form.get('suno_title', '').strip(),

View file

@ -133,8 +133,18 @@
<!-- Section 1: Customer info summary --> <!-- Section 1: Customer info summary -->
<div class="section"> <div class="section">
<h2>Customer Info</h2> <h2>Customer Info</h2>
<div class="info-grid"> <p class="copy-hint">Operators can correct the customer's email address here.</p>
<div><strong>Email:</strong> {{ req.email }}</div> <form method="POST">
<input type="hidden" name="action" value="update_customer_email">
<label for="customer_email">Email address</label>
<input type="email" id="customer_email" name="email" value="{{ req.email }}" required>
<div class="actions">
<button type="submit" class="secondary">Update Email</button>
</div>
</form>
<div class="info-grid" style="margin-top:1rem">
<div><strong>Request #:</strong> {{ req.id }}</div>
<div><strong>Status:</strong> {{ statuses[req.status] }}</div> <div><strong>Status:</strong> {{ statuses[req.status] }}</div>
</div> </div>
<p><strong>Hobbies:</strong><br>{{ req.hobbies or '-' }}</p> <p><strong>Hobbies:</strong><br>{{ req.hobbies or '-' }}</p>