docs: update comments and README to reflect current feature set

- Refresh app.py module and route docstrings for runtime settings,
  MP3 tagging, rate limiting, revision workflow, and admin actions.
- Clarify config.py and models.py comments.
- Update template comments/CSS for dashboard filters, request detail,
  player page, and settings page.
- Rewrite README.md with current features, status flow, file layout,
  deployment variables, and troubleshooting.
- Refresh REVIEW.md quick-reference.
- Add MAX_REVISIONS to docker-compose.yml environment list.
- Expand requirements.txt comment coverage.

No version history or changelog included.
This commit is contained in:
Troll (Hermes Agent) 2026-08-02 00:07:24 +00:00
parent 14ed80fe4b
commit 4af9322c9d
11 changed files with 148 additions and 111 deletions

View file

@ -1,16 +1,37 @@
# Theme Song Booth
Custom theme-song request and delivery system for a convention booth. Customers fill out a form, the operator generates two AI-made song versions, the customer picks one, and the approved MP3 is delivered by email after payment is collected.
A Flask web app for a convention booth where visitors request a custom AI-generated theme song, the operator manages the queue, and the final MP3(s) are delivered by email after payment.
## What this project does
## Features
- **Customer request page** (`/request`) — booth visitors enter their name, email, hobbies, notable facts, preferred genre, and extra requests. A branded banner image is shown.
- **Operator dashboard** (`/admin`) — queue of all requests with status filters, per-request detail page, and system reset.
- **Prompt generation** — the admin page builds a plain-text prompt for Hermes/AI, which returns a Title, Style, and Lyrics block. The operator pastes that response, clicks **Extract**, then uses Copy buttons to paste into Suno Custom Mode.
- **Song upload** — operator uploads Version A and Version B MP3s.
- **Customer player page** — a private `/play/<token>` page emails to the customer. They can listen to both versions, choose A/B/both, or request changes.
- **Payment and delivery** — operator enters a Square payment reference and clicks **Mark Paid & Deliver**. The approved MP3(s) are emailed as attachments.
- **System reset** — one button in the admin topbar clears all requests and files at the start of an event.
### 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.
- **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.
- **Rate limiting** — the public request form is capped at 5 submissions per minute per IP.
### Operator/admin
- **Admin login** (`/admin/login`) — simple session-based login protected by `ADMIN_PASSWORD`.
- **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.
- 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.
- **Settings / maintenance page** (`/admin/settings`):
- Database health check with optional schema repair.
- Database statistics, size, upload counts.
- Configure customer revision limit.
- Configure dashboard auto-refresh interval.
- Configure SMTP host/port/user/from; store the SMTP password encrypted.
- Configure default MP3 metadata tags (artist, album, year, comment).
- Send a test email.
- Download or restore the SQLite database backup.
- Reset the entire system for a new event.
- **Per-request delete** and **system reset** — remove requests and uploaded files; reset auto-increment back to 1.
## Status flow
@ -20,33 +41,36 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
| Status | Meaning |
|---|---|
| `pending` | Customer submitted request; no prompt yet. |
| `pending` | Customer submitted a request; operator has not saved a prompt yet. |
| `prompt_ready` | Operator saved Title/Style/Lyrics. |
| `songs_uploaded` | Both MP3s uploaded; preview link can be sent. |
| `awaiting_payment` | Customer approved a version. |
| `paid` | Payment reference recorded; delivery email sent. |
| `delivered` | MP3 attachments emailed. |
| `songs_uploaded` | Both MP3s uploaded; preview link can be sent. Dashboard filter label: **Needs Upload** (shown for this state when filtering). |
| `revisions_requested` | Customer asked for changes; current files were archived. |
| `awaiting_payment` | Customer approved a version; waiting for operator to collect payment and deliver. |
| `paid` | Payment recorded (used internally). |
| `delivered` | MP3 attachment(s) emailed to the customer. |
## File layout
| File | Purpose |
|---|---|
| `app.py` | Flask routes, helpers, and email logic. |
| `config.py` | Environment-variable based configuration. |
| `models.py` | SQLite schema and database helper functions. |
| `app.py` | Flask routes, helpers, email layer, runtime settings, MP3 tagging, rate limiting, and DB maintenance helpers. |
| `config.py` | Environment-variable based configuration; defines defaults for DB, uploads, SMTP, and secrets. |
| `models.py` | SQLite schema and CRUD helpers. |
| `init_db.py` | Standalone script to create the database tables. |
| `templates/request.html` | Customer request form (with banner). |
| `templates/request.html` | Customer request form. |
| `templates/thanks.html` | Post-submission confirmation. |
| `templates/player.html` | Customer audio player and approval page. |
| `templates/admin/login.html` | Admin password login. |
| `templates/admin/dashboard.html` | Operator queue with filters and reset. |
| `templates/admin/request.html` | Single-request detail / prompt / upload / delivery. |
| `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/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. |
| `Dockerfile` | Production container image. |
| `docker-compose.yml` | Portainer stack definition. |
| `requirements.txt` | Python dependencies. |
| `.env.example` | Template for environment variables. |
| `REVIEW.md` | Quick reference for returning to this project. |
| `.env.example` | Template for local environment variables. |
| `REVIEW.md` | Quick-reference for returning to this project. |
## Local development
@ -76,18 +100,19 @@ Visit:
| Variable | Required | Purpose |
|---|---|---|
| `APP_SECRET_KEY` | Yes | Long random string for Flask sessions. Generate with `python3 -c "import secrets; print(secrets.token_hex(32))"`. |
| `APP_SECRET_KEY` | Yes | Long random string for Flask sessions and to encrypt stored SMTP password. Generate with `python3 -c "import secrets; print(secrets.token_hex(32))"`. |
| `ADMIN_PASSWORD` | Yes | Password for `/admin`. |
| `SMTP_PASS` | Yes | Password for `ai@hallsworth.ca`. |
| `SMTP_PASS` | Yes | Password for the SMTP account. |
| `PUBLIC_BASE_URL` | Yes | Public HTTPS URL, e.g. `https://booth.dionysismedia.ca`. |
| `HOST_PORT` | No | Host-side port mapping, default `127.0.0.1:8000`. |
| `INTERNAL_PORT` | No | Port gunicorn binds inside container, default `8000`. |
| `BOOTH_NAME` | No | Name used in emails, default `Trollgorithm Theme Songs`. |
| `PRICE_PER_VERSION` | No | Shown on receipt page, default `10.00`. |
| `PRICE_PER_VERSION` | No | Shown to the operator/customer, default `10.00`. |
| `CURRENCY` | No | Currency label, default `CAD`. |
| `MAX_REVISIONS` | No | Default customer revision limit if not changed in settings, default `2`. |
5. Deploy the stack.
6. Open a console in the `booth` container and run once:
6. Open a console in the `theme-song-booth` container and run once:
```bash
python init_db.py
@ -103,8 +128,11 @@ After each push to GitLab, go to Portainer → **Stacks** → `theme-song-booth`
## Important notes
- **No `.env` file in production.** `docker-compose.yml` passes variables directly from Portainer. This avoids Portainer's `env_file not found` error.
- **Runtime settings persist.** SMTP config, revision limit, auto-refresh interval, and MP3 metadata defaults are stored encrypted (where sensitive) in `booth_settings.json` inside the persistent uploads volume. They survive redeploys.
- **Payments are manual.** The app records a Square payment reference but does not integrate with Square's API. Use a Square Terminal/Reader at the booth.
- **Operator queue is the dashboard.** No operator email alerts are sent; approvals and revision notes appear as status changes in `/admin`.
- **MP3 metadata.** Uploaded files are tagged with title (from the saved prompt), plus configured artist/album/year/comment values.
- **Email logo.** `static/DM-Logo_email.png` is attached inline to all customer emails as the Dionysis Media signature.
- **Security:** the repo is public on GitLab. No secrets are committed. Admin password is plain text in the Portainer environment.
## Common troubleshooting
@ -112,9 +140,11 @@ After each push to GitLab, go to Portainer → **Stacks** → `theme-song-booth`
| Problem | Cause | Fix |
|---|---|---|
| "Send Preview Link" does nothing | Form tags were unbalanced (now fixed). | Redeploy the latest commit. |
| Emails not arriving | SMTP_PASS wrong or messages in spam. | Verify SMTP credentials; check spam folder. |
| Emails not arriving | SMTP settings wrong or messages in spam. | Use **Send Test Email** on `/admin/settings`; verify host/port/password. |
| Can't reach app through domain | Reverse proxy points to wrong host port. | Match `HOST_PORT` to your proxy upstream. |
| Static banner not showing | Browser cached old image. | Hard-refresh or redeploy stack. |
| Logo missing from email | Logo file missing from `static/`. | Ensure `static/DM-Logo_email.png` is in the container. |
| Database schema mismatch | New column added but old DB not migrated. | Go to `/admin/settings` and click **Fix Missing Columns**, or run `python init_db.py`. |
## License / ownership

View file

@ -14,6 +14,9 @@ Flask app that lets convention attendees request custom AI-generated theme songs
- Portainer stack deployed from GitLab repo
- SMTP (SSL port 465) for customer emails
- Square Terminal/Reader for manual payment
- `mutagen` for MP3 metadata tagging
- `flask-limiter` for public form rate limiting
- `cryptography` to encrypt the stored SMTP password
## Repository
@ -24,12 +27,13 @@ Flask app that lets convention attendees request custom AI-generated theme songs
| File | Notes |
|---|---|
| `app.py` | All routes, helpers, email function, status labels. |
| `app.py` | All routes, helpers, email function, status labels, runtime settings, MP3 tagging, rate limiting, DB health. |
| `config.py` | Env vars. `ADMIN_PASSWORD` is plain text. |
| `models.py` | SQLite schema + CRUD. `player_token` is a secret URL-safe token. |
| `init_db.py` | Run once after deploy: `python init_db.py`. |
| `templates/admin/request.html` | Biggest template; prompt extraction JS lives here. |
| `templates/admin/dashboard.html` | Queue table + topbar Reset System button. |
| `templates/admin/request.html` | Biggest template; prompt copy helpers and JS live here. |
| `templates/admin/dashboard.html` | Queue table + filters + auto-refresh + topbar Reset System button. |
| `templates/admin/settings.html` | SMTP config, MP3 metadata defaults, DB backup/restore, health check, reset. |
| `docker-compose.yml` | No `env_file`; variables come from Portainer. |
## Status meanings
@ -38,12 +42,14 @@ Flask app that lets convention attendees request custom AI-generated theme songs
pending → prompt_ready → songs_uploaded → awaiting_payment → paid → delivered
```
`revisions_requested` is a branch used when the customer asks for changes.
## Operator workflow
1. Customer fills `/request`.
2. Open `/admin`, click request row.
3. Click **Copy customer info for Hermes**, paste result to Hermes.
4. Paste Hermes response (Title/Style/Lyrics format), click **Extract**, click **Save Prompt**.
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.
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.
7. Click **Send Preview Link**.
@ -61,34 +67,23 @@ PUBLIC_BASE_URL
BOOTH_NAME
HOST_PORT
INTERNAL_PORT
MAX_REVISIONS
```
Most can be overridden at runtime from `/admin/settings` and stored in `booth_settings.json`.
## Gotchas
- Multiple forms on `admin/request.html` must stay properly closed; nested forms break buttons.
- `upload_songs` form needs `enctype="multipart/form-data"` and a matching `</form>`.
- The dashboard uses `basename()` as a function, not a Jinja filter.
- Reset System deletes DB rows **and** all files under `UPLOAD_FOLDER`.
## Things that could be improved later
- Move customer info copy/paste to a direct Hermes API/webhook call.
- Add operator email alerts as an opt-in config instead of hard-disabled.
- Store admin password hashed.
- Add a receipt/pricing page for the customer.
- Upload progress indicator for large MP3s.
- Back up SQLite and uploads to S3 or similar before reset.
- Reset System deletes DB rows **and** all files under `UPLOAD_FOLDER`, then resets `sqlite_sequence`.
- Runtime settings are stored in the persistent uploads volume (`booth_settings.json`).
- Container cannot read host paths; all static assets used at runtime (logo, banner, favicons) must be in the repo or a mounted volume.
## How to redeploy
1. Push changes to GitLab `main`.
2. In Portainer: Stacks → `theme-song-booth` → Pull and redeploy.
3. If schema changed, open container console and run `python init_db.py`.
2. In Portainer: Stacks → `theme-song-booth`**Pull and redeploy**.
3. If schema changed, open container console and run `python init_db.py`, or use `/admin/settings`**Fix Missing Columns**.
## Last major changes
- Added banner image and styling to request page.
- Moved Reset System button to topbar next to Log out.
- Added file/email status badges on admin request page.
- Added per-request Delete and full-system Reset.
- Switched Hermes prompt workflow to plain-text Title/Style/Lyrics blocks.

79
app.py
View file

@ -3,26 +3,29 @@ app.py
======
Main Flask application for the Theme Song Booth.
This module defines all HTTP routes, helper functions, and the email layer.
This module defines all HTTP routes, helper functions, the email layer,
runtime settings persistence, MP3 metadata tagging, rate limiting, and
database health/maintenance helpers.
It is meant to be served by gunicorn inside a Docker container (see Dockerfile).
Public routes (customers):
- / -> redirects to /request
- /request -> customer submits their info
- /thanks/<id> -> confirmation page after submission
- /play/<token> -> private player page with Version A and B
- /play/<token>/approve -> customer picks a version
- /play/<token>/revise -> customer asks for changes
- / -> redirects to /request
- /request -> customer submits their info
- /thanks/<id> -> confirmation page after submission
- /play/<token> -> private player page with Version A and B
- /play/<token>/approve -> customer picks a version
- /play/<token>/revise -> customer asks for changes
- /audio/<token>/<v>.mp3 -> serves the uploaded MP3 files
Admin routes:
- /admin/login -> password login
- /admin/logout -> clears session
- /admin -> dashboard queue
- /admin/settings -> health check, DB stats, disk usage, system reset
- /admin/request/<id> -> detail/edit page for a single request
- /admin/request/<id>/delete -> deletes one request and its files
- /admin/reset -> deletes ALL requests and ALL files
- /admin/login -> password login
- /admin/logout -> clears session
- /admin -> dashboard queue with status filters and auto-refresh
- /admin/settings -> runtime settings, health check, DB stats, backup/restore, reset
- /admin/request/<id> -> detail/edit page for a single request
- /admin/request/<id>/delete -> deletes one request and its uploaded files
- /admin/reset -> deletes ALL requests and ALL files
"""
# Standard library imports
@ -65,7 +68,7 @@ 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.
# Global request rate limiting by remote IP (default 60/min). The public form is further limited to 5/min.
limiter = Limiter(get_remote_address, app=app, default_limits=["60 per minute"])
# Ensure the SQLite connection is closed at the end of each request.
@ -136,8 +139,10 @@ def save_upload(request_id, file_obj, version, song_title=None):
def apply_mp3_tags(path, title=None):
"""
Write or overwrite common ID3 tags on an MP3 file using values from
runtime booth settings. The saved Suno title is written to the Title tag.
Write common ID3 tags on an uploaded MP3 using the runtime metadata defaults.
Writes title, artist, album, and date via EasyID3, plus a comment using both
a COMM frame and a TXXX:Comment frame for broad reader compatibility.
Failures are logged as a warning and do not block the upload.
"""
cfg = load_booth_settings()
try:
@ -230,7 +235,8 @@ def save_booth_settings(settings):
def get_email_config():
"""
Return the effective SMTP configuration.
Runtime-encrypted settings from disk override env defaults.
Runtime settings in booth_settings.json override environment defaults.
The SMTP password is decrypted from the encrypted value stored on disk.
"""
cfg = load_booth_settings()
return {
@ -251,16 +257,6 @@ def get_refresh_seconds():
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'
try:
cfg_path.write_text(json.dumps(settings, indent=2))
except OSError as e:
flash(f'Warning: could not save settings: {e}', 'error')
def send_email(to, subject, body, attachments=None, inline_images=None):
"""Send an email using the configured or runtime SMTP settings."""
cfg = get_email_config()
@ -299,7 +295,7 @@ def send_email(to, subject, body, attachments=None, inline_images=None):
server.send_message(msg)
def build_signature_images():
"""Return inline image tuple list for the Dionysis Media logo."""
"""Return inline image tuple list for static/DM-Logo_email.png (Dionysis Media logo)."""
logo_path = Path(current_app.root_path) / 'static' / 'DM-Logo_email.png'
if not logo_path.exists():
return []
@ -322,7 +318,9 @@ def request_form():
"""
Public request form.
GET -> shows the form with the banner image.
POST -> creates a database record and redirects to the thanks page.
POST -> 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':
rid = create_request(
@ -380,6 +378,7 @@ def play(token):
"""
Private player page for a customer.
The token is a cryptographically random URL-safe string generated at request time.
Shows A/B audio players, approval buttons, or a revision note depending on status.
"""
req = get_request_by_token(token)
if not req:
@ -398,6 +397,7 @@ def approve(token):
"""
Customer has chosen Version A, Version B, or both.
Updates the request status to 'awaiting_payment' so the operator can collect payment.
Operator email alerts are intentionally disabled; the dashboard is the single queue.
"""
req = get_request_by_token(token)
if not req:
@ -409,10 +409,6 @@ def approve(token):
update_request(req['id'], customer_approved=choice, status='awaiting_payment', approval_notified_at=now_utc())
# NOTE: Operator email alerts are intentionally disabled. The admin dashboard is the single queue.
# alert_to = current_app.config['ADMIN_ALERT_EMAIL'] or current_app.config['SMTP_FROM']
# if alert_to: ...
flash('Thanks! Please return to the booth to finalize payment.', 'success')
return redirect(url_for('play', token=token))
@ -420,8 +416,9 @@ def approve(token):
@app.route('/play/<token>/revise', methods=['POST'])
def revise(token):
"""
Customer asked for changes. Store the note and reset status to 'songs_uploaded'
so the operator sees it in the dashboard queue.
Customer asked for changes.
Enforces the runtime max revisions limit, archives the current A/B MP3 files,
stores the revision note, and resets status to 'revisions_requested'.
"""
note = request.form.get('revision_note', '').strip()
req = get_request_by_token(token)
@ -506,6 +503,7 @@ def admin_dashboard():
"""
Main operator queue.
Optional ?status= filter lets operators focus on one state at a time.
Auto-refresh interval is controlled from /admin/settings.
"""
redir = require_admin()
if redir:
@ -519,9 +517,10 @@ def admin_dashboard():
def admin_request(rid):
"""
Detail/edit page for a single request.
GET -> render the request details and editing forms.
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
Uploaded MP3s are tagged with metadata defaults from /admin/settings.
"""
redir = require_admin()
if redir:
@ -658,8 +657,10 @@ def admin_delete_request(rid):
def admin_settings():
"""
Settings / maintenance page for operators.
GET -> show database health, statistics, disk usage, and reset button.
POST -> either run a health check/fix or reset the system.
GET -> show database health, statistics, disk usage, runtime settings forms,
SMTP/email config, MP3 metadata defaults, backup/restore, and reset.
POST -> handle one of: fix_db, reset_system, save_max_revisions, save_metadata,
save_email_config, send_test_email, save_refresh, download_db, restore_db.
"""
redir = require_admin()
if redir:

View file

@ -50,12 +50,12 @@ class Config:
# 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')
# Booth name used in email sign-offs.
# Booth name used in customer-facing text and email sign-offs.
BOOTH_NAME = os.environ.get('BOOTH_NAME', 'Trollgorithm Theme Songs')
# Internal port gunicorn listens on inside the container.
# Internal port gunicorn listens on inside the container (also exposed in Dockerfile).
INTERNAL_PORT = int(os.environ.get('INTERNAL_PORT', '8000'))
# Price per version shown on the receipt page (informational only; payment is manual).
# Informational price shown to the operator/customer; actual payment is collected manually (e.g. Square).
PRICE_PER_VERSION = float(os.environ.get('PRICE_PER_VERSION', '10.00'))
CURRENCY = os.environ.get('CURRENCY', 'CAD')

View file

@ -28,6 +28,7 @@ services:
- PUBLIC_BASE_URL=${PUBLIC_BASE_URL}
- BOOTH_NAME=${BOOTH_NAME:-Trollgorithm Theme Songs}
- INTERNAL_PORT=${INTERNAL_PORT:-8000}
- MAX_REVISIONS=${MAX_REVISIONS:-2}
- PRICE_PER_VERSION=${PRICE_PER_VERSION:-10.00}
- CURRENCY=${CURRENCY:-CAD}
- DATABASE=${DATABASE:-/app/data/booth.db}

View file

@ -8,7 +8,8 @@ application context (`g`) is used to manage one connection per request.
Schema overview (see SCHEMA constant):
- requests table stores customer data, generated prompts, file paths,
approval state, email timestamps, payment reference, and player token.
approval state, email timestamps, payment reference, player token,
vocal gender preference, revision count, and revision notes.
- Indexes on status and player_token for fast queue/lookup.
"""
@ -40,7 +41,7 @@ CREATE TABLE IF NOT EXISTS requests (
preview_sent_at TIMESTAMP,
delivery_sent_at TIMESTAMP,
square_payment_ref TEXT,
admin_alert_email TEXT,
admin_alert_email TEXT, -- reserved for future operator alerts; currently unused
player_token TEXT NOT NULL UNIQUE,
revision_count INTEGER DEFAULT 0,
revision_note TEXT
@ -115,7 +116,9 @@ def get_request_by_token(token):
def list_requests(status=None):
"""List all requests, optionally filtered by status, newest first."""
"""List all requests, optionally filtered by status, newest first.
Normalizes whitespace in the status parameter so URLs like "Needs Upload"
match the stored value."""
db = get_db()
if status:
# Translate human filter names to stored status values.
@ -141,7 +144,8 @@ def update_request(request_id, **fields):
def delete_request(request_id):
"""Delete a single request by id. Does NOT delete associated files."""
"""Delete a single request row by id. Does NOT delete associated files
(the caller in app.py removes uploads before/after this call)."""
db = get_db()
db.execute('DELETE FROM requests WHERE id = ?', (request_id,))
db.commit()

View file

@ -3,10 +3,13 @@
#
# Python dependencies for the Theme Song Booth Flask app.
#
# flask - web framework
# gunicorn - production WSGI server used by Dockerfile
# python-dotenv - loads .env files in development
# werkzeug - utilities for file uploads and password hashing
# flask - web framework
# gunicorn - production WSGI server used by Dockerfile
# python-dotenv - loads .env files in development
# werkzeug - utilities for file uploads and secure filenames
# mutagen - MP3 metadata (ID3) tagging
# flask-limiter - public form rate limiting
# cryptography - encrypt stored SMTP password
flask
gunicorn

View file

@ -127,6 +127,7 @@
<!-- Status filter links -->
<div class="filters">
<a href="{{ url_for('admin_dashboard') }}" class="{% if current_status in (None, '') %}active{% endif %}">All</a>
<a href="{{ url_for('admin_dashboard', status='pending') }}" class="{% if current_status == 'pending' %}active{% endif %}">Pending</a>
<a href="{{ url_for('admin_dashboard', status='songs_uploaded') }}" class="{% if current_status == 'songs_uploaded' %}active{% endif %}">Needs Upload</a>
<a href="{{ url_for('admin_dashboard', status='awaiting_payment') }}" class="{% if current_status == 'awaiting_payment' %}active{% endif %}">Awaiting Payment</a>
<a href="{{ url_for('admin_dashboard', status='delivered') }}" class="{% if current_status == 'delivered' %}active{% endif %}">Delivered</a>

View file

@ -9,10 +9,10 @@
Single-request admin detail page.
Sections:
1. Customer info (with revisions note if any)
2. Generate Suno prompt
3. Upload Songs
4. Notify Customer
5. Payment & Delivery
2. Generate and save Suno prompt
3. Upload Version A and Version B MP3s
4. Send preview email with private player link
5. Mark paid and deliver selected MP3(s)
*/
body{
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
@ -154,7 +154,7 @@
<!-- Section 2: Generate Suno prompt -->
<div class="section">
<h2>1. Generate Suno Prompt</h2>
<h2>1. Generate & Save Suno Prompt</h2>
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save.</p>

View file

@ -7,7 +7,8 @@
<style>
/*
Admin settings / maintenance page.
Two-column layout for settings; table-styled MP3 metadata fields.
Two-column layout for revision limit, auto-refresh, SMTP config,
MP3 metadata defaults, database backup/restore, health stats, and reset.
*/
:root{
--bg:#0b0f19;

View file

@ -11,6 +11,7 @@
plus approval buttons or a revision note form.
After the customer makes a choice, the controls are hidden
and a confirmation/waiting message is shown instead.
The number of remaining revisions is shown when available.
*/
body{
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
@ -120,7 +121,7 @@
</div>
{% else %}
<!-- Approval form: customer picks A, B, or both -->
<!-- Approval form: customer picks Version A, B, or both -->
<form method="POST" action="{{ url_for('approve', token=req.player_token) }}">
<input type="hidden" name="choice" id="choice">
<div class="actions">