# REVIEW.md — Theme Song Booth Quick reference for future work on this project. ## One-sentence summary Flask app that lets convention attendees request custom AI-generated theme songs, lets an operator manage the queue, and emails MP3s after payment. ## Stack - Python 3.12 + Flask - SQLite (file-based, request-scoped connection via `g`) - Gunicorn in Docker - Portainer stack deployed from Gitea 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 - Gitea: `https://gitlab.hallsworth.ca/yrtria/theme-song-booth` (public) - Deployed at: `https://booth.dionysismedia.ca` ## Key files and what they hold | File | Notes | |---|---| | `app.py` | All routes, helpers, email function, status labels, runtime settings, MP3 tagging, rate limiting, DB health, kiosk route, pricing route, sales report, ZIP backup, and the `/api/prompt/` Hermes callback endpoint. | | `config.py` | Env vars. `ADMIN_PASSWORD` is plain text. `HERMES_API_KEY` can be overridden at runtime. | | `models.py` | SQLite schema + CRUD. `player_token` is a secret URL-safe token. `revision_history` table tracks each customer revision. | | `init_db.py` | Run once after deploy: `python init_db.py`. | | `templates/admin/request.html` | Biggest template. Customer info editing, revision history, operator notes, Suno prompt fields, upload/delivery, cancel request button. | | `templates/admin/dashboard.html` | Queue table + filters + auto-refresh + topbar links to Kiosk, Pricing, Sales, Settings. | | `templates/admin/settings.html` | SMTP config, MP3 metadata defaults, DB backup/restore/health, reset, kiosk mode, Hermes API key display. | | `templates/admin/pricing.html` | Fixed and custom pricing configuration. | | `templates/admin/sales.html` | Sales report of delivered requests. | | `templates/faq.html` | Customer FAQ page. | | `templates/status.html` | Customer order status lookup. | | `templates/closed.html` | Message shown on `/request` when the booth is marked closed. | | `templates/kiosk.html` | Public full-screen display: open/closed banner, QR code, price list, auto-refresh. | | `docker-compose.yml` | No `env_file`; variables come from Portainer. | ## Status meanings ``` pending → prompt_ready → songs_uploaded → awaiting_payment → paid → delivered ``` Branch states: - `revisions_requested` — customer asked for changes; current files are archived and a note is logged. - `cancelled` — operator cancelled the request. ## Operator workflow 1. Customer fills `/request`. 2. Open `/admin`, click request row (or filter by status). 3. On `/admin/request/`, fix customer info if needed, then click **Copy customer info for Hermes**, paste result to Hermes. 4. Hermes POSTs Title/Style/Lyrics back to the signed callback URL; the request becomes **Prompt Ready**. 5. If the callback fails, paste Hermes' response into the Title/Style/Lyrics fields and click **Save Prompt**. 6. Copy Lyrics, Style, Title into Suno Custom Mode in that order, generate two versions. 7. Upload Version A and B MP3s. 8. Click **Send Preview Link**. 9. Customer receives email, visits player, picks version. 10. Operator collects Square payment, enters reference, clicks **Mark Paid & Deliver**. 11. Customer receives MP3 attachment(s) by email. ## Environment variables that matter ``` APP_SECRET_KEY ADMIN_PASSWORD SMTP_PASS PUBLIC_BASE_URL BOOTH_NAME HOST_PORT INTERNAL_PORT MAX_REVISIONS HERMES_API_KEY ``` Most can be overridden at runtime from `/admin/settings` and stored in `booth_settings.json`. ## Runtime settings stored in `booth_settings.json` - SMTP host/port/user/from and encrypted password - Dashboard auto-refresh interval - Max revisions allowed per customer - Booth open/closed state - Default MP3 metadata tags (artist, album, year, comment) - Hermes API key - Pricing: fixed items (One Song, Both Songs, WAV per song, STEMs per song) and up to 5 custom named items - Kiosk cycle mode (QR only, pricing only, or N seconds per slide) These survive redeploys because `booth_settings.json` lives in the persistent uploads volume. ## 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 ``. - The dashboard uses `basename()` as a function, not a Jinja filter. - 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`). - The `booth_open` setting controls whether `/request` and `/kiosk` show the open banner or the closed banner. - Container cannot read host paths; all static assets used at runtime (logo, banners, QR code) must be in `static/` or a mounted volume. - The Hermes callback URL is signed with `APP_SECRET_KEY` and expires after 7 days. - If you regenerate the Hermes API key, update the Hermes skill/config immediately; old key requests will 401. - New columns/tables are added via `models.py`. Use `/admin/settings` → **Fix Database Schema** after redeploying a schema change. - `__pycache__` and local `.env` files are already ignored by `.gitignore`; make sure they never get committed. ## How to redeploy 1. Push changes to Gitea `main`. 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 Database Schema**. ## Recent major additions - **Structured style dropdowns** — customer form now uses Decade / Basic / Additional style dropdowns instead of a free-text genre field. Values are stored as a comma-separated string in `style_genre`. - **Pronouns field** — required pronouns dropdown on the customer request form; stored in the `pronouns` column. - **Lyrics in delivery email** — final delivery email includes the generated lyrics in the same format as the player page. - **Delete uploaded songs** — admin request page can delete selected Version A / B uploads and reset the request to `prompt_ready`. - **Live queue kiosk slide** — `/kiosk` can cycle through QR, pricing, and active-queue slides based on `kiosk_cycle_seconds`. - **Cancelled status** — operators can mark any request as cancelled from the top of `/admin/request/`. - **Revision history log** — each customer revision is recorded with revision count, note, and archived file names. - **Stems / Extras link** — operators paste a file-share link on the request page; customers see a download button after delivery. - **Sales report** — `/admin/sales` lists all delivered requests with payment references. - **Pricing page** — `/admin/pricing` configures fixed prices plus up to 5 custom items; used by `/kiosk`. - **Public kiosk** — `/kiosk` is a full-screen tablet display with QR code and price list cycling. - **Music ZIP backup** — `/admin/settings` can download all uploaded MP3s as a ZIP. - **Database schema repair** — health check detects missing columns and tables and can repair them. ## Project state notes - No `.gitlab-ci.yml` is currently in the repo; old pipeline records from an earlier CI config are still visible in Gitea but are not actionable because no runners are attached. Add a CI skeleton (see below) if you want automated checks back. - No automated tests exist yet. ## CI skeleton (optional) A **CI skeleton** is the smallest Gitea CI config that gives you useful automated checks on every push without needing a heavy test suite. For this project it would be a `.gitlab-ci.yml` with one or two jobs: 1. **Syntax check job** — install Python dependencies and run `python -m py_compile app.py models.py config.py init_db.py` to catch SyntaxErrors before they reach Portainer. 2. **(Optional) Test job** — run a minimal pytest suite once tests are written. Right now this would be a placeholder that skips if no tests exist, so the pipeline stays green while you decide whether to add tests. It needs a Gitea runner to execute. Your Gitea instance has no runners attached, which is why the old pipelines are stuck/canceled. The skeleton just defines *what* to run; a runner is still required for it to actually execute. ## Static assets to keep in the repo - `static/Trollgorithm_booth.jpg` — open banner (request page and kiosk) - `static/Booth_closed.png` — closed banner - `static/DM-Logo_email.png` — email signature logo - `static/qr-code.png` — kiosk QR code pointing to `/request`