diff --git a/README.md b/README.md index 4b86648..4606fc5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Theme Song Booth -**Version:** `v0.5.5` +**Version:** `v0.5.6` A Flask web application for running a convention booth where visitors request a custom AI-generated theme song. Operators manage the queue from an admin dashboard, generate Suno prompts, upload MP3 previews, collect payment, and deliver final songs by email. diff --git a/REVIEW.md b/REVIEW.md index 49d7185..8f975e8 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -11,7 +11,7 @@ Flask app that lets convention attendees request custom AI-generated theme songs - Python 3.12 + Flask - SQLite (file-based, request-scoped connection via `g`) - Gunicorn in Docker -- Portainer stack deployed from GitLab repo +- Portainer stack deployed from Gitea repo - SMTP (SSL port 465) for customer emails - Square Terminal/Reader for manual payment - `mutagen` for MP3 metadata tagging @@ -20,7 +20,7 @@ Flask app that lets convention attendees request custom AI-generated theme songs ## Repository -- GitLab: `https://gitlab.hallsworth.ca/yrtria/theme-song-booth` (public) +- Gitea: `https://gitlab.hallsworth.ca/yrtria/theme-song-booth` (public) - Deployed at: `https://booth.dionysismedia.ca` ## Key files and what they hold @@ -112,7 +112,7 @@ These survive redeploys because `booth_settings.json` lives in the persistent up ## How to redeploy -1. Push changes to GitLab `main`. +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**. @@ -134,17 +134,17 @@ These survive redeploys because `booth_settings.json` lives in the persistent up ## Project state notes -- No `.gitlab-ci.yml` is currently in the repo; old pipeline records from an earlier CI config are still visible in GitLab but are not actionable because no runners are attached. Add a CI skeleton (see below) if you want automated checks back. +- 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 GitLab 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: +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 GitLab runner to execute. Your GitLab 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. +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 diff --git a/VERSION b/VERSION index d1d899f..b49b253 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.5 +0.5.6 diff --git a/app.py b/app.py index 560529b..099fc82 100644 --- a/app.py +++ b/app.py @@ -64,6 +64,9 @@ from models import SCHEMA, get_requests_by_email, log_revision, list_revision_hi from mutagen.mp3 import MP3 from mutagen.easyid3 import EasyID3 +# HTTP client for ntfy push notifications +import requests + # --------------------------------------------------------------------------- # Genre / decade helpers @@ -411,6 +414,36 @@ def mask_api_key(key): return '*' * (len(key) - 6) + key[-6:] +def get_ntfy_config(): + """Return the effective ntfy server URL and topic from runtime settings.""" + cfg = load_booth_settings() + return { + 'server': cfg.get('ntfy_server', ''), + 'topic': cfg.get('ntfy_topic', ''), + } + + +def send_ntfy(message, title='Theme Song Booth', priority='default', tags='bell'): + """Send a push notification to the configured ntfy topic, if configured.""" + ntfy = get_ntfy_config() + server = ntfy.get('server', '').rstrip('/') + topic = ntfy.get('topic', '').strip() + if not server or not topic: + return False + + url = f"{server}/{topic}" + headers = { + 'Title': title, + 'Priority': priority, + 'Tags': tags, + } + try: + resp = requests.post(url, data=message.encode('utf-8'), headers=headers, timeout=10) + return resp.status_code in (200, 202) + except Exception: + return False + + def sign_prompt_callback(rid, expires_at=None): """ Create a signed callback token for a specific request ID. @@ -559,6 +592,18 @@ def request_form(): return render_template('request.html', form=form_data, decades=DECADES, genres=MUSIC_GENRES), 400 rid = create_request(**form_data) + # Notify operator via ntfy when a new request comes in. + try: + send_ntfy( + f"New request #{rid} from {form_data['name']} ({form_data['email']})\nStyle: {form_data['style_genre'] or '-'}", + title='New Theme Song Request', + priority='high', + tags='musical_note' + ) + except Exception: + # Push notification failure should never break the customer form. + pass + # Send confirmation email with a summary of what the customer asked for. req = get_request_by_id(rid) if req: @@ -1626,6 +1671,28 @@ def admin_settings(): flash('Hermes API key is managed via the HERMES_API_KEY environment variable.', 'info') return redirect(url_for('admin_settings')) + elif action == 'save_ntfy': + # Update ntfy push notification server/topic from the settings form. + cfg = load_booth_settings() + cfg['ntfy_server'] = request.form.get('ntfy_server', '').strip().rstrip('/') + cfg['ntfy_topic'] = request.form.get('ntfy_topic', '').strip() + save_booth_settings(cfg) + flash('ntfy notification settings saved.', 'success') + return redirect(url_for('admin_settings')) + + elif action == 'send_test_ntfy': + # Send a test push notification to the configured ntfy topic. + ntfy = get_ntfy_config() + if not ntfy['server'] or not ntfy['topic']: + flash('Configure ntfy server and topic first.', 'error') + return redirect(url_for('admin_settings')) + ok = send_ntfy('This is a test push notification from the Trollgorithm Theme Song Booth.', title='Test Notification', priority='high', tags='test_tube') + if ok: + flash('Test ntfy notification sent.', 'success') + else: + flash('Failed to send ntfy test notification. Check server and topic.', 'error') + return redirect(url_for('admin_settings')) + return render_template( 'admin/settings.html', health=health, @@ -1653,6 +1720,7 @@ def admin_settings(): hermes_key_set=hermes_key_set, hermes_key_just_generated=hermes_key_just_generated, version=current_app.config['VERSION'], + ntfy=runtime_settings, ) diff --git a/requirements.txt b/requirements.txt index 16f50f1..c5b0ac1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ werkzeug mutagen flask-limiter cryptography +requests diff --git a/templates/admin/settings.html b/templates/admin/settings.html index 2051b4c..395da38 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -293,6 +293,34 @@ {% endif %} + +
+

ntfy Notifications

+

Push notifications for new customer requests. Leave either field blank to disable.

+
+ + + + + + + + + + +
ntfy Server URL
ntfy Topic
+ +
+ +
+ +

Send Test Notification

+
+ + +
+
+

Email (SMTP) Settings