From 51d1b4a4d9bef844ec877c44def425ed4d0aa214 Mon Sep 17 00:00:00 2001 From: "Troll (Hermes Agent)" Date: Sat, 1 Aug 2026 20:55:20 +0000 Subject: [PATCH] Add admin Settings page with DB health check, stats, and system reset --- app.py | 117 ++++++++++++++++++- templates/admin/dashboard.html | 13 ++- templates/admin/settings.html | 208 +++++++++++++++++++++++++++++++++ 3 files changed, 333 insertions(+), 5 deletions(-) create mode 100644 templates/admin/settings.html diff --git a/app.py b/app.py index 11f6487..1778556 100644 --- a/app.py +++ b/app.py @@ -19,6 +19,7 @@ Admin routes: - /admin/login -> password login - /admin/logout -> clears session - /admin -> dashboard queue +- /admin/settings -> health check, DB stats, disk usage, system reset - /admin/request/ -> detail/edit page for a single request - /admin/request//delete -> deletes one request and its files - /admin/reset -> deletes ALL requests and ALL files @@ -38,7 +39,8 @@ 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 +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 # --------------------------------------------------------------------------- # App setup @@ -425,6 +427,119 @@ def admin_delete_request(rid): return redirect(url_for('admin_dashboard')) +@app.route('/admin/settings', methods=['GET', 'POST']) +def admin_settings(): + """ + Settings / maintenance page for operators. + GET -> show database health, statistics, disk usage, and reset button. + POST -> either run a health check/fix or reset the system. + """ + redir = require_admin() + if redir: + return redir + + db_path = Path(current_app.config['DATABASE']) + upload_root = Path(current_app.config['UPLOAD_FOLDER']) + + # Compute database stats. + db_size = db_path.stat().st_size if db_path.exists() else 0 + all_requests = list_requests() + total_records = len(all_requests) + status_counts = {} + for req in all_requests: + status_counts[req['status']] = status_counts.get(req['status'], 0) + 1 + + # Compute upload folder stats. + total_upload_size = 0 + upload_file_count = 0 + request_dir_count = 0 + if upload_root.exists(): + for entry in upload_root.iterdir(): + if entry.is_dir(): + request_dir_count += 1 + for f in entry.iterdir(): + if f.is_file(): + total_upload_size += f.stat().st_size + upload_file_count += 1 + elif entry.is_file(): + total_upload_size += entry.stat().st_size + upload_file_count += 1 + + def format_bytes(n): + for unit in ['B', 'KB', 'MB', 'GB']: + if n < 1024: + return f"{n:.2f} {unit}" + n /= 1024 + return f"{n:.2f} TB" + + # Health check: verify expected columns exist. + expected_cols = { + 'id', 'created_at', 'name', 'email', 'hobbies', 'notable_facts', + 'style_genre', 'extra_requests', 'status', 'suno_title', 'suno_style', + 'suno_lyrics', 'song_a_path', 'song_b_path', 'customer_approved', + 'approval_notified_at', 'preview_sent_at', 'delivery_sent_at', + 'square_payment_ref', 'admin_alert_email', 'player_token', 'revision_note' + } + health = {'ok': True, 'missing_columns': [], 'message': 'Database schema looks good.'} + try: + db = get_db() + cur = db.execute('PRAGMA table_info(requests)') + existing_cols = {row['name'] for row in cur.fetchall()} + missing = sorted(expected_cols - existing_cols) + if missing: + health = {'ok': False, 'missing_columns': missing, 'message': f'Missing columns: {", ".join(missing)}'} + except Exception as e: + health = {'ok': False, 'missing_columns': [], 'message': f'Could not inspect table: {e}'} + + if request.method == 'POST': + 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() + 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') + return redirect(url_for('admin_settings')) + + elif action == 'reset_system': + # Same nuclear reset logic as the old /admin/reset endpoint. + if upload_root.exists(): + for entry in upload_root.iterdir(): + try: + if entry.is_file(): + entry.unlink() + elif entry.is_dir(): + shutil.rmtree(entry) + except OSError: + pass + reset_all_requests() + flash('System reset complete. All orders and files have been cleared.', 'success') + return redirect(url_for('admin_settings')) + + return render_template( + 'admin/settings.html', + health=health, + db_size=format_bytes(db_size), + total_records=total_records, + status_counts=status_counts, + statuses=STATUS_LABELS, + upload_file_count=upload_file_count, + upload_dir_count=request_dir_count, + upload_size=format_bytes(total_upload_size), + db_path=str(db_path), + upload_path=str(upload_root) + ) + + @app.route('/admin/reset', methods=['POST']) def admin_reset_system(): """ diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index 2bbde8a..24da000 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -93,6 +93,13 @@ font-size:.9rem; } .topbar button.reset-sm:hover{background:#b91c1c;} + .topbar a{ + color:#93c5fd; + text-decoration:none; + } + .topbar a.settings{ + color:#10b981; + } /* Hidden legacy reset box (kept CSS class for compatibility, not displayed) */ .reset-box{display:none;} @@ -100,11 +107,9 @@
- +
-
- -
+ Settings Log out
diff --git a/templates/admin/settings.html b/templates/admin/settings.html new file mode 100644 index 0000000..53f815a --- /dev/null +++ b/templates/admin/settings.html @@ -0,0 +1,208 @@ + + + + + + Admin Settings + + + +
+ + +

Admin Settings

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endwith %} + + +
+

Database Health

+ {% if health.ok %} +

✅ {{ health.message }}

+ {% else %} +

❌ {{ health.message }}

+ {% if health.missing_columns %} +

Missing columns:

+
    + {% for col in health.missing_columns %} +
  • {{ col }}
  • + {% endfor %} +
+
+ + +
+ {% endif %} + {% endif %} +

DB path: {{ db_path }}

+
+ + +
+

Database Statistics

+
+
+
{{ total_records }}
+
Total requests
+
+
+
{{ db_size }}
+
Database size
+
+
+
{{ upload_file_count }}
+
Uploaded MP3 files
+
+
+
{{ upload_dir_count }}
+
Request upload folders
+
+
+
{{ upload_size }}
+
Total upload size
+
+
+ + {% if status_counts %} +

By Status

+
    + {% for key,count in status_counts.items() %} +
  • {{ statuses[key] }} ({{ key }}): {{ count }}
  • + {% endfor %} +
+ {% else %} +

No requests yet.

+ {% endif %} +
+ + +
+

Paths

+

Database:
{{ db_path }}

+

Uploads:
{{ upload_path }}

+
+ + +
+

⚠️ Reset System

+

Use this only at the start of a new event. It will delete every request and every uploaded MP3 file. This cannot be undone.

+
+ + +
+
+
+ +