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
- **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.
- **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.
@ -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.
- **Per-request detail page** (`/admin/request/<id>`):
- 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).
- Send a preview email with a private player link.
- 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/admin/login.html` | Admin login page. |
| `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. |
| `static/Trollgorithm_booth.jpg` | Banner image on the request page. |
| `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`.
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**.
5. Copy Style/Lyrics into Suno Custom Mode, generate two versions.
6. Upload Version A and B MP3s.

40
app.py
View file

@ -30,6 +30,7 @@ Admin routes:
# Standard library imports
import os
import re
import shutil
import smtplib
import ssl
@ -38,7 +39,6 @@ from email.message import EmailMessage
from pathlib import Path
import json
import time
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
@ -318,14 +318,18 @@ def request_form():
"""
Public request form.
GET -> shows the form with the banner image.
POST -> creates a database record, sends a confirmation email,
and redirects to the thanks page.
POST -> validates the email, creates a database record, sends a
confirmation email, and redirects to the thanks page.
Rate limited to 5 submissions per minute per IP.
"""
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(
name=request.form.get('name', '').strip(),
email=request.form.get('email', '').strip(),
email=email,
hobbies=request.form.get('hobbies', '').strip(),
notable_facts=request.form.get('notable_facts', '').strip(),
style_genre=request.form.get('style_genre', '').strip(),
@ -491,6 +495,15 @@ def admin_login():
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')
def admin_logout():
"""Clear the admin session."""
@ -517,9 +530,11 @@ def admin_dashboard():
def admin_request(rid):
"""
Detail/edit page for a single request.
GET -> render customer info, prompt, upload status, email status, and delivery forms.
POST -> handle one of four actions:
save_prompt, upload_songs, notify_customer, mark_paid_deliver
GET -> render customer info (email editable), prompt, upload status,
email status, and delivery forms.
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.
"""
redir = require_admin()
@ -549,7 +564,16 @@ def admin_request(rid):
if request.method == 'POST':
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.
update_request(rid,
suno_title=request.form.get('suno_title', '').strip(),

View file

@ -133,8 +133,18 @@
<!-- Section 1: Customer info summary -->
<div class="section">
<h2>Customer Info</h2>
<div class="info-grid">
<div><strong>Email:</strong> {{ req.email }}</div>
<p class="copy-hint">Operators can correct the customer's email address here.</p>
<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>
<p><strong>Hobbies:</strong><br>{{ req.hobbies or '-' }}</p>