feat: add operator-only notes field per request

This commit is contained in:
Troll (Hermes Agent) 2026-08-02 22:10:42 +00:00
parent 41ca43b9c2
commit 3e25c3638b
5 changed files with 38 additions and 17 deletions

View file

@ -69,7 +69,7 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
| `templates/player.html` | Customer audio player, approval, and revision form. | | `templates/player.html` | Customer audio player, approval, and revision form. |
| `templates/admin/login.html` | Admin login page. | | `templates/admin/login.html` | Admin login page. |
| `templates/admin/dashboard.html` | Operator queue with filters and auto-refresh. | | `templates/admin/dashboard.html` | Operator queue with filters and auto-refresh. |
| `templates/admin/request.html` | Single-request detail / prompt / upload / delivery page. Customer email is editable here. | | `templates/admin/request.html` | Single-request detail / prompt / upload / delivery page. Customer email and operator notes are editable here. |
| `templates/admin/settings.html` | Maintenance, settings, backup/restore, and reset page. | | `templates/admin/settings.html` | Maintenance, settings, backup/restore, and reset page. |
| `static/Trollgorithm_booth.jpg` | Banner image on the request page. | | `static/Trollgorithm_booth.jpg` | Banner image on the request page. |
| `static/DM-Logo_email.png` | Inline Dionysis Media logo attached to emails. | | `static/DM-Logo_email.png` | Inline Dionysis Media logo attached to emails. |

View file

@ -31,7 +31,7 @@ Flask app that lets convention attendees request custom AI-generated theme songs
| `config.py` | Env vars. `ADMIN_PASSWORD` is plain text. | | `config.py` | Env vars. `ADMIN_PASSWORD` is plain text. |
| `models.py` | SQLite schema + CRUD. `player_token` is a secret URL-safe token. | | `models.py` | SQLite schema + CRUD. `player_token` is a secret URL-safe token. |
| `init_db.py` | Run once after deploy: `python init_db.py`. | | `init_db.py` | Run once after deploy: `python init_db.py`. |
| `templates/admin/request.html` | Biggest template; prompt copy helpers and JS live here. Customer email is editable here. | | `templates/admin/request.html` | Biggest template; prompt copy helpers and JS live here. Customer email and operator notes are editable here. |
| `templates/admin/dashboard.html` | Queue table + filters + auto-refresh + topbar Reset System button. | | `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/admin/settings.html` | SMTP config, MP3 metadata defaults, DB backup/restore, health check, reset. |
| `templates/faq.html` | Customer FAQ page. | | `templates/faq.html` | Customer FAQ page. |

13
app.py
View file

@ -572,9 +572,9 @@ def admin_request(rid):
""" """
Detail/edit page for a single request. Detail/edit page for a single request.
GET -> render customer info (email editable), prompt, upload status, GET -> render customer info (email editable), prompt, upload status,
email status, and delivery forms. email status, operator notes, and delivery forms.
POST -> handle one of five actions: POST -> handle one of six actions:
update_customer_email, save_prompt, upload_songs, update_customer_email, save_operator_notes, save_prompt, upload_songs,
notify_customer, mark_paid_deliver notify_customer, mark_paid_deliver
Uploaded MP3s are tagged with metadata defaults from /admin/settings. Uploaded MP3s are tagged with metadata defaults from /admin/settings.
""" """
@ -614,6 +614,11 @@ def admin_request(rid):
flash('Customer email updated.', 'success') flash('Customer email updated.', 'success')
return redirect(url_for('admin_request', rid=rid)) return redirect(url_for('admin_request', rid=rid))
elif action == 'save_operator_notes':
update_request(rid, operator_notes=request.form.get('operator_notes', '').strip())
flash('Operator notes saved.', 'success')
return redirect(url_for('admin_request', rid=rid))
elif action == 'save_prompt': elif action == 'save_prompt':
# Store the generated title/style/lyrics and mark prompt ready. # Store the generated title/style/lyrics and mark prompt ready.
update_request(rid, update_request(rid,
@ -786,7 +791,7 @@ def admin_settings():
'style_genre', 'extra_requests', 'vocal_gender', 'status', 'suno_title', 'suno_style', 'style_genre', 'extra_requests', 'vocal_gender', 'status', 'suno_title', 'suno_style',
'suno_lyrics', 'song_a_path', 'song_b_path', 'customer_approved', 'suno_lyrics', 'song_a_path', 'song_b_path', 'customer_approved',
'approval_notified_at', 'preview_sent_at', 'delivery_sent_at', 'approval_notified_at', 'preview_sent_at', 'delivery_sent_at',
'square_payment_ref', 'admin_alert_email', 'player_token', 'revision_note', 'revision_count' 'square_payment_ref', 'admin_alert_email', 'player_token', 'revision_note', 'revision_count', 'operator_notes'
} }
health = {'ok': True, 'missing_columns': [], 'message': 'Database schema looks good.'} health = {'ok': True, 'missing_columns': [], 'message': 'Database schema looks good.'}
try: try:

View file

@ -44,7 +44,8 @@ CREATE TABLE IF NOT EXISTS requests (
admin_alert_email TEXT, -- reserved for future operator alerts; currently unused admin_alert_email TEXT, -- reserved for future operator alerts; currently unused
player_token TEXT NOT NULL UNIQUE, player_token TEXT NOT NULL UNIQUE,
revision_count INTEGER DEFAULT 0, revision_count INTEGER DEFAULT 0,
revision_note TEXT revision_note TEXT,
operator_notes TEXT
); );
CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status); CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status);

View file

@ -9,10 +9,11 @@
Single-request admin detail page. Single-request admin detail page.
Sections: Sections:
1. Customer info (with revisions note if any) 1. Customer info (with revisions note if any)
2. Generate and save Suno prompt 2. Operator notes (internal, operator-only)
3. Upload Version A and Version B MP3s 3. Generate and save Suno prompt
4. Send preview email with private player link 4. Upload Version A and Version B MP3s
5. Mark paid and deliver selected MP3(s) 5. Send preview email with private player link
6. Mark paid and deliver selected MP3(s)
*/ */
body{ body{
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
@ -162,9 +163,23 @@
{% endif %} {% endif %}
</div> </div>
<!-- Section 2: Generate Suno prompt --> <!-- Section 2: Operator notes -->
<div class="section"> <div class="section">
<h2>1. Generate & Save Suno Prompt</h2> <h2>Operator Notes</h2>
<p class="copy-hint">Internal notes for the booth team. Customers cannot see this.</p>
<form method="POST">
<input type="hidden" name="action" value="save_operator_notes">
<label for="operator_notes">Notes</label>
<textarea id="operator_notes" name="operator_notes" rows="4" placeholder="e.g. paid cash, VIP friend, follow up...">{{ req.operator_notes or '' }}</textarea>
<div class="actions">
<button type="submit" class="secondary">Save Notes</button>
</div>
</form>
</div>
<!-- Section 3: Generate Suno prompt -->
<div class="section">
<h2>1. Generate &amp; Save Suno Prompt</h2>
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button> <button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save.</p> <p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save.</p>
@ -190,7 +205,7 @@
</form> </form>
</div> </div>
<!-- Section 3: Upload songs --> <!-- Section 4: Upload songs -->
<div class="section"> <div class="section">
<h2>2. Upload Songs</h2> <h2>2. Upload Songs</h2>
<p> <p>
@ -222,7 +237,7 @@
</form> </form>
</div> </div>
<!-- Section 4: Notify customer --> <!-- Section 5: Notify customer -->
<div class="section"> <div class="section">
<h2>3. Notify Customer</h2> <h2>3. Notify Customer</h2>
@ -248,9 +263,9 @@
{% endif %} {% endif %}
</div> </div>
<!-- Section 5: Payment & delivery --> <!-- Section 6: Payment &amp; delivery -->
<div class="section"> <div class="section">
<h2>4. Payment & Delivery</h2> <h2>4. Payment &amp; Delivery</h2>
<p> <p>
Customer approved: Customer approved:
{% if req.customer_approved == 'none' %} {% if req.customer_approved == 'none' %}