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
|
||||
|
||||
**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.
|
||||
|
||||
|
|
|
|||
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
|
||||
- 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
|
||||
|
||||
|
|
|
|||
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.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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,3 +18,4 @@ werkzeug
|
|||
mutagen
|
||||
flask-limiter
|
||||
cryptography
|
||||
requests
|
||||
|
|
|
|||
|
|
@ -293,6 +293,34 @@
|
|||
{% endif %}
|
||||
</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 -->
|
||||
<div class="section">
|
||||
<h2>Email (SMTP) Settings</h2>
|
||||
|
|
|
|||
Reference in a new issue