Add admin Settings page with DB health check, stats, and system reset

This commit is contained in:
Troll (Hermes Agent) 2026-08-01 20:55:20 +00:00
parent 2b960bd3d5
commit 51d1b4a4d9
3 changed files with 333 additions and 5 deletions

117
app.py
View file

@ -19,6 +19,7 @@ Admin routes:
- /admin/login -> password login - /admin/login -> password login
- /admin/logout -> clears session - /admin/logout -> clears session
- /admin -> dashboard queue - /admin -> dashboard queue
- /admin/settings -> health check, DB stats, disk usage, system reset
- /admin/request/<id> -> detail/edit page for a single request - /admin/request/<id> -> detail/edit page for a single request
- /admin/request/<id>/delete -> deletes one request and its files - /admin/request/<id>/delete -> deletes one request and its files
- /admin/reset -> deletes ALL requests and ALL files - /admin/reset -> deletes ALL requests and ALL files
@ -38,7 +39,8 @@ from werkzeug.utils import secure_filename
# Project imports # Project imports
from config import Config 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 # App setup
@ -425,6 +427,119 @@ def admin_delete_request(rid):
return redirect(url_for('admin_dashboard')) 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']) @app.route('/admin/reset', methods=['POST'])
def admin_reset_system(): def admin_reset_system():
""" """

View file

@ -93,6 +93,13 @@
font-size:.9rem; font-size:.9rem;
} }
.topbar button.reset-sm:hover{background:#b91c1c;} .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) */ /* Hidden legacy reset box (kept CSS class for compatibility, not displayed) */
.reset-box{display:none;} .reset-box{display:none;}
@ -100,11 +107,9 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<!-- Topbar: Reset System button and Log out link --> <!-- Topbar: Settings link and Log out link -->
<div class="topbar"> <div class="topbar">
<form method="POST" action="{{ url_for('admin_reset_system') }}" onsubmit="return confirm('ARE YOU SURE? This will delete ALL requests and ALL uploaded files. This cannot be undone.')"> <a href="{{ url_for('admin_settings') }}" class="settings">Settings</a>
<button type="submit" class="reset-sm">Reset System</button>
</form>
<a href="{{ url_for('admin_logout') }}" class="logout">Log out</a> <a href="{{ url_for('admin_logout') }}" class="logout">Log out</a>
</div> </div>

View file

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Settings</title>
<style>
/*
Admin settings / maintenance page.
Shows database health, statistics, upload disk usage,
and houses the dangerous system reset button.
*/
body{
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background:#111827;
color:#f3f4f6;
margin:0;
padding:1rem;
line-height:1.5;
}
.container{max-width:900px;margin:0 auto;}
h1,h2{color:#60a5fa;}
a{color:#93c5fd;text-decoration:none;}
.topbar{
float:right;
display:flex;
gap:.75rem;
align-items:center;
}
.topbar a{
color:#f87171;
}
.section{
background:#1f2937;
padding:1rem;
border-radius:.5rem;
margin-bottom:1rem;
}
.flash{
padding:.8rem;
background:#064e3b;
border-radius:.5rem;
margin-bottom:1rem;
}
.flash.error{background:#450a0a;}
.stat-grid{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
gap:1rem;
}
.stat-card{
background:#111827;
padding:.8rem;
border-radius:.5rem;
}
.stat-card .value{
font-size:1.5rem;
font-weight:700;
color:#60a5fa;
}
.stat-card .label{
font-size:.85rem;
color:#9ca3af;
}
.health-ok{
color:#10b981;
font-weight:700;
}
.health-bad{
color:#f87171;
font-weight:700;
}
.status-list{
margin:0;
padding-left:1.2rem;
}
.status-list li{
margin-bottom:.3rem;
}
button{
padding:.7rem 1rem;
border:none;
border-radius:.5rem;
background:#3b82f6;
color:#fff;
font-weight:700;
cursor:pointer;
margin-right:.5rem;
}
button.fix{
background:#10b981;
}
button.danger{
background:#dc2626;
}
.danger-zone{
background:#450a0a;
border:1px solid #7f1d1d;
border-radius:.5rem;
padding:1rem;
}
.danger-zone h2{
color:#f87171;
margin-top:0;
}
.path{
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
font-size:.85rem;
word-break:break-all;
color:#9ca3af;
}
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<a href="{{ url_for('admin_dashboard') }}">Dashboard →</a>
<a href="{{ url_for('admin_logout') }}">Log out</a>
</div>
<h1>Admin Settings</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
<!-- Database health -->
<div class="section">
<h2>Database Health</h2>
{% if health.ok %}
<p class="health-ok">✅ {{ health.message }}</p>
{% else %}
<p class="health-bad">❌ {{ health.message }}</p>
{% if health.missing_columns %}
<p>Missing columns:</p>
<ul class="status-list">
{% for col in health.missing_columns %}
<li>{{ col }}</li>
{% endfor %}
</ul>
<form method="POST" style="margin-top:.8rem">
<input type="hidden" name="action" value="fix_db">
<button type="submit" class="fix">Fix Missing Columns</button>
</form>
{% endif %}
{% endif %}
<p class="path">DB path: {{ db_path }}</p>
</div>
<!-- Statistics -->
<div class="section">
<h2>Database Statistics</h2>
<div class="stat-grid">
<div class="stat-card">
<div class="value">{{ total_records }}</div>
<div class="label">Total requests</div>
</div>
<div class="stat-card">
<div class="value">{{ db_size }}</div>
<div class="label">Database size</div>
</div>
<div class="stat-card">
<div class="value">{{ upload_file_count }}</div>
<div class="label">Uploaded MP3 files</div>
</div>
<div class="stat-card">
<div class="value">{{ upload_dir_count }}</div>
<div class="label">Request upload folders</div>
</div>
<div class="stat-card">
<div class="value">{{ upload_size }}</div>
<div class="label">Total upload size</div>
</div>
</div>
{% if status_counts %}
<h3 style="margin-top:1.2rem;color:#93c5fd">By Status</h3>
<ul class="status-list">
{% for key,count in status_counts.items() %}
<li>{{ statuses[key] }} ({{ key }}): <strong>{{ count }}</strong></li>
{% endfor %}
</ul>
{% else %}
<p style="margin-top:1rem;color:#9ca3af">No requests yet.</p>
{% endif %}
</div>
<!-- Paths -->
<div class="section">
<h2>Paths</h2>
<p><strong>Database:</strong><br><span class="path">{{ db_path }}</span></p>
<p><strong>Uploads:</strong><br><span class="path">{{ upload_path }}</span></p>
</div>
<!-- System reset -->
<div class="danger-zone">
<h2>⚠️ Reset System</h2>
<p>Use this only at the start of a new event. It will delete every request and every uploaded MP3 file. This cannot be undone.</p>
<form method="POST" action="{{ url_for('admin_settings') }}" onsubmit="return confirm('ARE YOU SURE? This will delete ALL requests and ALL uploaded files. This cannot be undone.')">
<input type="hidden" name="action" value="reset_system">
<button type="submit" class="danger">Reset System</button>
</form>
</div>
</div>
</body>
</html>