feat: add booth open/closed toggle on settings page
This commit is contained in:
parent
c39265ab42
commit
5e26650b90
6 changed files with 96 additions and 6 deletions
|
|
@ -6,7 +6,8 @@ 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. The email field is validated to reduce delivery problems.
|
||||
- **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. If the operator marks the booth as closed, this page shows a closed banner and message instead.
|
||||
- **Closed page** (`/request`) — when the booth is marked closed from `/admin/settings`, visitors see the closed banner and a friendly "goblin engineers are on a break" message.
|
||||
- **Confirmation page** (`/thanks/<id>`) — shows the request number after submission.
|
||||
- **FAQ page** (`/faq`) — answers common customer questions.
|
||||
- **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.
|
||||
|
|
@ -59,6 +60,7 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
|
|||
| `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/closed.html` | Message shown on `/request` when the booth is marked closed. |
|
||||
| `templates/request.html` | Customer request form. |
|
||||
| `templates/thanks.html` | Post-submission confirmation. |
|
||||
| `templates/faq.html` | Customer FAQ page. |
|
||||
|
|
@ -69,6 +71,7 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
|
|||
| `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. |
|
||||
| `static/Booth_closed.png` | Banner shown when the booth is marked closed. |
|
||||
| `Dockerfile` | Production container image. |
|
||||
| `docker-compose.yml` | Portainer stack definition. |
|
||||
| `requirements.txt` | Python dependencies. |
|
||||
|
|
@ -131,7 +134,8 @@ 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.
|
||||
- **Runtime settings persist.** SMTP config, revision limit, auto-refresh interval, booth open/closed state, and MP3 metadata defaults are stored in `booth_settings.json` inside the persistent uploads volume. They survive redeploys.
|
||||
- **Booth open/closed switch.** Operators can flip the booth status from `/admin/settings`. When closed, `/request` shows a closed banner and message instead of the form.
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ Flask app that lets convention attendees request custom AI-generated theme songs
|
|||
| `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. |
|
||||
| `templates/faq.html` | Customer FAQ page. |
|
||||
| `templates/closed.html` | Message shown on `/request` when the booth is marked closed. |
|
||||
| `docker-compose.yml` | No `env_file`; variables come from Portainer. |
|
||||
|
||||
## Status meanings
|
||||
|
|
@ -80,7 +81,8 @@ Most can be overridden at runtime from `/admin/settings` and stored in `booth_se
|
|||
- 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`).
|
||||
- Container cannot read host paths; all static assets used at runtime (logo, banner, favicons) must be in the repo or a mounted volume.
|
||||
- The `booth_open` setting controls whether `/request` shows the form or the closed banner.
|
||||
- Container cannot read host paths; all static assets used at runtime (logo, banner, favicons, closed banner) must be in the repo or a mounted volume.
|
||||
|
||||
## How to redeploy
|
||||
|
||||
|
|
|
|||
26
app.py
26
app.py
|
|
@ -308,6 +308,12 @@ def build_signature_images():
|
|||
return [(str(logo_path), 'dm-logo')]
|
||||
|
||||
|
||||
def get_booth_open():
|
||||
"""Return True if the booth is currently marked as open in runtime settings."""
|
||||
cfg = load_booth_settings()
|
||||
return cfg.get('booth_open', True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public customer routes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -323,11 +329,14 @@ def index():
|
|||
def request_form():
|
||||
"""
|
||||
Public request form.
|
||||
GET -> shows the form with the banner image.
|
||||
GET -> shows the form with the banner image, or a closed message if the booth is closed.
|
||||
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 not get_booth_open():
|
||||
return render_template('closed.html')
|
||||
|
||||
if request.method == 'POST':
|
||||
form_data = {
|
||||
'name': request.form.get('name', '').strip(),
|
||||
|
|
@ -694,9 +703,9 @@ def admin_settings():
|
|||
"""
|
||||
Settings / maintenance page for operators.
|
||||
GET -> show database health, statistics, disk usage, runtime settings forms,
|
||||
SMTP/email config, MP3 metadata defaults, backup/restore, and reset.
|
||||
booth open/closed switch, 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.
|
||||
save_email_config, send_test_email, save_refresh, save_booth_open, download_db, restore_db.
|
||||
"""
|
||||
redir = require_admin()
|
||||
if redir:
|
||||
|
|
@ -717,6 +726,7 @@ def admin_settings():
|
|||
runtime_settings = load_booth_settings()
|
||||
current_max_revisions = runtime_settings.get('max_revisions', current_app.config.get('MAX_REVISIONS', 2))
|
||||
current_refresh_seconds = runtime_settings.get('refresh_seconds', 10)
|
||||
booth_open = runtime_settings.get('booth_open', True)
|
||||
|
||||
# Effective email config to show in the form (non-sensitive only; password left blank).
|
||||
email_form = {
|
||||
|
|
@ -868,6 +878,15 @@ def admin_settings():
|
|||
flash(f'Dashboard auto-refresh set to {val} seconds.', 'success')
|
||||
return redirect(url_for('admin_settings'))
|
||||
|
||||
elif action == 'save_booth_open':
|
||||
# Toggle whether the public request form is accepting submissions.
|
||||
cfg = load_booth_settings()
|
||||
cfg['booth_open'] = request.form.get('booth_open', '1') == '1'
|
||||
save_booth_settings(cfg)
|
||||
state = 'open' if cfg['booth_open'] else 'closed'
|
||||
flash(f'Booth is now {state}.', 'success')
|
||||
return redirect(url_for('admin_settings'))
|
||||
|
||||
elif action == 'download_db':
|
||||
# Send the SQLite database file as a download.
|
||||
if db_path.exists():
|
||||
|
|
@ -915,6 +934,7 @@ def admin_settings():
|
|||
upload_path=str(upload_root),
|
||||
current_max_revisions=current_max_revisions,
|
||||
current_refresh_seconds=current_refresh_seconds,
|
||||
booth_open=booth_open,
|
||||
email_form=email_form,
|
||||
metadata=runtime_settings,
|
||||
)
|
||||
|
|
|
|||
BIN
static/Booth_closed.png
Executable file
BIN
static/Booth_closed.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
|
|
@ -171,6 +171,21 @@
|
|||
|
||||
<div class="grid">
|
||||
|
||||
<!-- Database health check -->
|
||||
<div class="section">
|
||||
<h2>Booth Status</h2>
|
||||
<p class="copy-hint">When closed, the public request page shows a "booth closed" message instead of the form.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_booth_open">
|
||||
<label for="booth_open">Booth is currently</label>
|
||||
<select id="booth_open" name="booth_open">
|
||||
<option value="1" {% if booth_open %}selected{% endif %}>Open ✅</option>
|
||||
<option value="0" {% if not booth_open %}selected{% endif %}>Closed ❌</option>
|
||||
</select>
|
||||
<button type="submit">Save Booth Status</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Database health check -->
|
||||
<div class="section">
|
||||
<h2>Database Health</h2>
|
||||
|
|
|
|||
49
templates/closed.html
Normal file
49
templates/closed.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trollgorithm Theme Song Booth — Closed</title>
|
||||
<style>
|
||||
/*
|
||||
Closed booth page.
|
||||
Friendly message shown when the booth is temporarily not taking requests.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#111827 0%,#1e3a8a 50%,#111827 100%);
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.6;
|
||||
min-height:100vh;
|
||||
}
|
||||
.container{max-width:680px;margin:0 auto;text-align:center;}
|
||||
.banner{
|
||||
width:100%;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
.card{
|
||||
background:rgba(31,41,55,.9);
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 25px rgba(0,0,0,.3);
|
||||
border:1px solid rgba(96,165,250,.2);
|
||||
}
|
||||
h1{margin-top:0;color:#60a5fa;font-size:1.7rem;}
|
||||
p{font-size:1.1rem;color:#d1d5db;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="{{ url_for('static', filename='Booth_closed.png') }}" alt="Trollgorithm booth is closed" class="banner">
|
||||
<div class="card">
|
||||
<h1>Trollgorithm and the goblin engineers are on a break.</h1>
|
||||
<p>Please come back in a while and they should be back to work!</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in a new issue