feat: add ntfy push notifications for new requests
- Add ntfy server/topic settings in /admin/settings next to Hermes API key - POST a push notification to the configured topic on every new customer request - Add a Send Test ntfy button to verify configuration - Add requests dependency for ntfy HTTP calls - Bump version to 0.5.6
This commit is contained in:
parent
410a9cf021
commit
a56f6e498b
6 changed files with 105 additions and 8 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Theme Song Booth
|
# 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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
12
REVIEW.md
12
REVIEW.md
|
|
@ -11,7 +11,7 @@ Flask app that lets convention attendees request custom AI-generated theme songs
|
||||||
- Python 3.12 + Flask
|
- Python 3.12 + Flask
|
||||||
- SQLite (file-based, request-scoped connection via `g`)
|
- SQLite (file-based, request-scoped connection via `g`)
|
||||||
- Gunicorn in Docker
|
- Gunicorn in Docker
|
||||||
- Portainer stack deployed from GitLab repo
|
- Portainer stack deployed from Gitea repo
|
||||||
- SMTP (SSL port 465) for customer emails
|
- SMTP (SSL port 465) for customer emails
|
||||||
- Square Terminal/Reader for manual payment
|
- Square Terminal/Reader for manual payment
|
||||||
- `mutagen` for MP3 metadata tagging
|
- `mutagen` for MP3 metadata tagging
|
||||||
|
|
@ -20,7 +20,7 @@ Flask app that lets convention attendees request custom AI-generated theme songs
|
||||||
|
|
||||||
## Repository
|
## 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`
|
- Deployed at: `https://booth.dionysismedia.ca`
|
||||||
|
|
||||||
## Key files and what they hold
|
## 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
|
## 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**.
|
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**.
|
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
|
## 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.
|
- No automated tests exist yet.
|
||||||
|
|
||||||
## CI skeleton (optional)
|
## 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.
|
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.
|
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
|
## Static assets to keep in the repo
|
||||||
|
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.5.5
|
0.5.6
|
||||||
|
|
|
||||||
68
app.py
68
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.mp3 import MP3
|
||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
|
|
||||||
|
# HTTP client for ntfy push notifications
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Genre / decade helpers
|
# Genre / decade helpers
|
||||||
|
|
@ -411,6 +414,36 @@ def mask_api_key(key):
|
||||||
return '*' * (len(key) - 6) + key[-6:]
|
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):
|
def sign_prompt_callback(rid, expires_at=None):
|
||||||
"""
|
"""
|
||||||
Create a signed callback token for a specific request ID.
|
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
|
return render_template('request.html', form=form_data, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||||
rid = create_request(**form_data)
|
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.
|
# Send confirmation email with a summary of what the customer asked for.
|
||||||
req = get_request_by_id(rid)
|
req = get_request_by_id(rid)
|
||||||
if req:
|
if req:
|
||||||
|
|
@ -1626,6 +1671,28 @@ def admin_settings():
|
||||||
flash('Hermes API key is managed via the HERMES_API_KEY environment variable.', 'info')
|
flash('Hermes API key is managed via the HERMES_API_KEY environment variable.', 'info')
|
||||||
return redirect(url_for('admin_settings'))
|
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(
|
return render_template(
|
||||||
'admin/settings.html',
|
'admin/settings.html',
|
||||||
health=health,
|
health=health,
|
||||||
|
|
@ -1653,6 +1720,7 @@ def admin_settings():
|
||||||
hermes_key_set=hermes_key_set,
|
hermes_key_set=hermes_key_set,
|
||||||
hermes_key_just_generated=hermes_key_just_generated,
|
hermes_key_just_generated=hermes_key_just_generated,
|
||||||
version=current_app.config['VERSION'],
|
version=current_app.config['VERSION'],
|
||||||
|
ntfy=runtime_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,4 @@ werkzeug
|
||||||
mutagen
|
mutagen
|
||||||
flask-limiter
|
flask-limiter
|
||||||
cryptography
|
cryptography
|
||||||
|
requests
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,34 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ntfy push notifications -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>ntfy Notifications</h2>
|
||||||
|
<p class="copy-hint">Push notifications for new customer requests. Leave either field blank to disable.</p>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="save_ntfy">
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td>ntfy Server URL</td>
|
||||||
|
<td><input type="text" id="ntfy_server" name="ntfy_server" value="{{ ntfy.get('ntfy_server', '') }}" placeholder="e.g. https://ntfy.hallsworth.ca"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ntfy Topic</td>
|
||||||
|
<td><input type="text" id="ntfy_topic" name="ntfy_topic" value="{{ ntfy.get('ntfy_topic', '') }}" placeholder="e.g. Troll-AI-ntfy"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<button type="submit">Save ntfy Settings</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
|
||||||
|
|
||||||
|
<h3>Send Test Notification</h3>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="send_test_ntfy">
|
||||||
|
<button type="submit">Send Test ntfy</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Email / SMTP configuration -->
|
<!-- Email / SMTP configuration -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Email (SMTP) Settings</h2>
|
<h2>Email (SMTP) Settings</h2>
|
||||||
|
|
|
||||||
Reference in a new issue