From cda4d57859abcf2f16a2d998423f6e734b670c37 Mon Sep 17 00:00:00 2001 From: "Troll (Hermes Agent)" Date: Tue, 4 Aug 2026 16:54:51 +0000 Subject: [PATCH] Add revision history log and music files ZIP backup --- app.py | 50 ++++++++++++++++++++++++++--------- models.py | 37 ++++++++++++++++++++++++++ templates/admin/request.html | 44 ++++++++++++++++++++++++++++-- templates/admin/settings.html | 10 +++++++ 4 files changed, 127 insertions(+), 14 deletions(-) diff --git a/app.py b/app.py index e32954f..7b8603c 100644 --- a/app.py +++ b/app.py @@ -58,7 +58,7 @@ from werkzeug.utils import secure_filename # Project imports from config import Config from models import init_db, close_db, create_request, get_request_by_id, get_request_by_token, list_requests, update_request, now_utc, delete_request, reset_all_requests, get_db -from models import SCHEMA, get_requests_by_email +from models import SCHEMA, get_requests_by_email, log_revision, list_revision_history # Audio metadata import from mutagen.mp3 import MP3 @@ -630,6 +630,8 @@ def revise(token): # Increment revision counter and archive current files before new versions are uploaded. new_count = current_count + 1 upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(req['id']) + old_a, old_b = req.get('song_a_path'), req.get('song_b_path') + new_a, new_b = old_a, old_b if upload_dir.exists(): for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')): path = req.get(field) @@ -638,9 +640,14 @@ def revise(token): archived = upload_dir / f"Rev{new_count}-{old.name}" try: old.rename(archived) + if field == 'song_a_path': + new_a = str(archived) + else: + new_b = str(archived) req[field] = str(archived) except OSError: pass + log_revision(req['id'], new_count, note, old_a=old_a, old_b=old_b, new_a=new_a, new_b=new_b) update_request(req['id'], revision_note=note, status='revisions_requested', song_a_path=req.get('song_a_path'), song_b_path=req.get('song_b_path'), customer_approved='none', revision_count=new_count) @@ -963,7 +970,7 @@ def admin_request(rid): return redirect(url_for('admin_request', rid=rid)) - return render_template('admin/request.html', req=req, statuses=STATUS_LABELS, file_exists=file_exists, basename=basename, extra_files=extra_files, callback_url=build_prompt_callback_url(rid)) + return render_template('admin/request.html', req=req, statuses=STATUS_LABELS, file_exists=file_exists, basename=basename, extra_files=extra_files, callback_url=build_prompt_callback_url(rid), revision_history=list_revision_history(rid)) @app.route('/admin/request//delete', methods=['POST']) @@ -1088,19 +1095,20 @@ def admin_settings(): action = request.form.get('action') if action == 'fix_db': - # Attempt to add missing columns via ALTER TABLE. - if not health['ok'] and health['missing_columns']: - try: - db = get_db() + # Attempt to create missing tables and add missing columns via ALTER TABLE. + try: + db = get_db() + db.executescript(SCHEMA) + if not health['ok'] and health['missing_columns']: for col in health['missing_columns']: # Default to TEXT columns; adequate for current schema. db.execute(f'ALTER TABLE requests ADD COLUMN {col} TEXT') - db.commit() - flash(f'Added missing columns: {", ".join(health["missing_columns"])}. Please refresh the page.', 'success') - except Exception as e: - flash(f'Failed to fix database: {e}', 'error') - else: - flash('No columns need fixing.', 'success') + flash(f'Created missing tables and added columns: {", ".join(health["missing_columns"])}. Please refresh the page.', 'success') + else: + flash('Database schema is up to date.', 'success') + db.commit() + except Exception as e: + flash(f'Failed to fix database: {e}', 'error') return redirect(url_for('admin_settings')) elif action == 'reset_system': @@ -1199,6 +1207,24 @@ def admin_settings(): flash('Database file not found.', 'error') return redirect(url_for('admin_settings')) + elif action == 'download_uploads_zip': + # Zip all files under UPLOAD_FOLDER and send as a download. + import zipfile + zip_path = db_path.with_suffix('.uploads-' + str(int(time.time())) + '.zip') + try: + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: + if upload_root.exists(): + for entry in upload_root.rglob('*'): + if entry.is_file(): + zf.write(str(entry), str(entry.relative_to(upload_root))) + return send_file(str(zip_path), as_attachment=True, download_name='theme-song-booth-uploads.zip') + except Exception as e: + flash(f'Failed to create uploads ZIP: {e}', 'error') + return redirect(url_for('admin_settings')) + finally: + if zip_path.exists(): + zip_path.unlink() + elif action == 'restore_db': # Replace the current database file with an uploaded SQLite backup. file_obj = request.files.get('db_backup') diff --git a/models.py b/models.py index 83d5b58..6b0d16e 100644 --- a/models.py +++ b/models.py @@ -55,6 +55,21 @@ CREATE TABLE IF NOT EXISTS requests ( CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status); CREATE INDEX IF NOT EXISTS idx_requests_token ON requests(player_token); + +CREATE TABLE IF NOT EXISTS revision_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + revision_count INTEGER NOT NULL, + note TEXT, + old_song_a_path TEXT, + old_song_b_path TEXT, + new_song_a_path TEXT, + new_song_b_path TEXT, + FOREIGN KEY (request_id) REFERENCES requests(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_revision_history_request ON revision_history(request_id); """ @@ -168,6 +183,28 @@ def delete_request(request_id): db.commit() +def log_revision(request_id, revision_count, note, old_a=None, old_b=None, new_a=None, new_b=None): + """Record a revision event in the revision_history table.""" + db = get_db() + db.execute( + """INSERT INTO revision_history + (request_id, revision_count, note, old_song_a_path, old_song_b_path, new_song_a_path, new_song_b_path) + VALUES (?, ?, ?, ?, ?, ?, ?)""", + (request_id, revision_count, note, old_a, old_b, new_a, new_b) + ) + db.commit() + + +def list_revision_history(request_id): + """Return all revision history rows for a request, oldest first.""" + db = get_db() + rows = db.execute( + 'SELECT * FROM revision_history WHERE request_id = ? ORDER BY created_at ASC', + (request_id,) + ).fetchall() + return [dict(r) for r in rows] + + def reset_all_requests(): """Delete every row in the requests table and reset id auto-increment.""" db = get_db() diff --git a/templates/admin/request.html b/templates/admin/request.html index 3ec1e43..1c9c636 100644 --- a/templates/admin/request.html +++ b/templates/admin/request.html @@ -110,6 +110,23 @@ margin-left:1.8rem; } .approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24;} + .history-list{ + margin:0; + padding:0; + list-style:none; + } + .history-list li{ + border-bottom:1px solid #374151; + padding:.6rem 0; + } + .history-list li:last-child{border-bottom:none;} + .history-list .meta{ + font-size:.8rem; + color:#9ca3af; + } + .history-list .note{ + margin:.3rem 0 0 0; + } /* Two-column layout */ .two-col{ @@ -291,7 +308,7 @@ {% endif %} - +

4. Payment & Delivery

@@ -344,13 +361,36 @@

- +
{% if req.customer_approved == 'none' %}

⚠️ Customer must approve a version before you can mark paid or deliver.

{% endif %}
+ + +
+

Revision History

+ {% if revision_history %} +
    + {% for h in revision_history %} +
  • +
    Revision #{{ h.revision_count }} — {{ h.created_at }} + {% if h.old_song_a_path or h.old_song_b_path %} +
    Archived: + {% if h.old_song_a_path %}
    A: {{ basename(h.old_song_a_path) }}{% endif %} + {% if h.old_song_b_path %}
    B: {{ basename(h.old_song_b_path) }}{% endif %} + {% endif %} +
    +

    {{ h.note or 'No note provided.' }}

    +
  • + {% endfor %} +
+ {% else %} +

No revisions recorded yet.

+ {% endif %} +
diff --git a/templates/admin/settings.html b/templates/admin/settings.html index 45d7be7..9382132 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -364,6 +364,16 @@ + +
+

Music Files Backup

+

Download all uploaded MP3 and archived revision files as a single ZIP.

+
+ + +
+
+

⚠️ Reset System