Format admin settings page with two-column layout and metadata table
This commit is contained in:
parent
f82cb34670
commit
e4de42efa5
2 changed files with 212 additions and 185 deletions
7
booth_settings.json
Normal file
7
booth_settings.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"artist": "Trollgorithm",
|
||||||
|
"album": null,
|
||||||
|
"year": null,
|
||||||
|
"genre": null,
|
||||||
|
"comment": null
|
||||||
|
}
|
||||||
|
|
@ -7,239 +7,259 @@
|
||||||
<style>
|
<style>
|
||||||
/*
|
/*
|
||||||
Admin settings / maintenance page.
|
Admin settings / maintenance page.
|
||||||
Shows database health, statistics, upload disk usage,
|
Two-column layout for settings; table-styled MP3 metadata fields.
|
||||||
and houses the dangerous system reset button.
|
|
||||||
*/
|
*/
|
||||||
|
:root{
|
||||||
|
--bg:#0b0f19;
|
||||||
|
--panel:#111827;
|
||||||
|
--text:#f3f4f6;
|
||||||
|
--muted:#9ca3af;
|
||||||
|
--accent:#60a5fa;
|
||||||
|
--accent-dark:#2563eb;
|
||||||
|
--danger:#dc2626;
|
||||||
|
--danger-dark:#991b1b;
|
||||||
|
--success:#10b981;
|
||||||
|
--warning:#f59e0b;
|
||||||
|
--border:#374151;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;}
|
||||||
body{
|
body{
|
||||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
|
||||||
background:#111827;
|
|
||||||
color:#f3f4f6;
|
|
||||||
margin:0;
|
margin:0;
|
||||||
padding:1rem;
|
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||||
|
background:var(--bg);
|
||||||
|
color:var(--text);
|
||||||
line-height:1.5;
|
line-height:1.5;
|
||||||
}
|
}
|
||||||
.container{max-width:900px;margin:0 auto;}
|
.container{max-width:1200px;margin:0 auto;padding:1.5rem;}
|
||||||
h1,h2{color:#60a5fa;}
|
|
||||||
a{color:#93c5fd;text-decoration:none;}
|
|
||||||
.topbar{
|
.topbar{
|
||||||
float:right;
|
|
||||||
display:flex;
|
display:flex;
|
||||||
gap:.75rem;
|
justify-content:space-between;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
|
margin-bottom:1.5rem;
|
||||||
}
|
}
|
||||||
.topbar a{
|
h1{margin:0;font-size:1.5rem;}
|
||||||
color:#f87171;
|
.nav a{
|
||||||
|
color:var(--accent);
|
||||||
|
text-decoration:none;
|
||||||
|
margin-left:1rem;
|
||||||
}
|
}
|
||||||
.section{
|
.messages p{
|
||||||
background:#1f2937;
|
padding:.75rem 1rem;
|
||||||
padding:1rem;
|
|
||||||
border-radius:.5rem;
|
border-radius:.5rem;
|
||||||
margin-bottom:1rem;
|
margin:0 0 1rem;
|
||||||
}
|
}
|
||||||
.flash{
|
.messages .success{background:#064e3b;color:#a7f3d0;}
|
||||||
padding:.8rem;
|
.messages .error{background:#450a0a;color:#fca5a5;}
|
||||||
background:#064e3b;
|
|
||||||
border-radius:.5rem;
|
/* Two-column grid for settings */
|
||||||
margin-bottom:1rem;
|
.grid{
|
||||||
}
|
|
||||||
.flash.error{background:#450a0a;}
|
|
||||||
.stat-grid{
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
|
grid-template-columns:repeat(2, 1fr);
|
||||||
gap:1rem;
|
gap:1.5rem;
|
||||||
}
|
}
|
||||||
.stat-card{
|
@media (max-width:900px){
|
||||||
background:#111827;
|
.grid{grid-template-columns:1fr;}
|
||||||
padding:.8rem;
|
|
||||||
border-radius:.5rem;
|
|
||||||
}
|
}
|
||||||
.stat-card .value{
|
|
||||||
font-size:1.5rem;
|
.section{
|
||||||
font-weight:700;
|
background:var(--panel);
|
||||||
color:#60a5fa;
|
border:1px solid var(--border);
|
||||||
|
border-radius:.75rem;
|
||||||
|
padding:1.25rem;
|
||||||
}
|
}
|
||||||
.stat-card .label{
|
.section h2{
|
||||||
font-size:.85rem;
|
margin-top:0;
|
||||||
color:#9ca3af;
|
font-size:1.15rem;
|
||||||
|
color:var(--accent);
|
||||||
}
|
}
|
||||||
.health-ok{
|
.section p{
|
||||||
color:#10b981;
|
margin:.5rem 0;
|
||||||
font-weight:700;
|
color:var(--muted);
|
||||||
|
font-size:.95rem;
|
||||||
}
|
}
|
||||||
.health-bad{
|
label{
|
||||||
color:#f87171;
|
display:block;
|
||||||
font-weight:700;
|
margin-top:.75rem;
|
||||||
|
font-size:.9rem;
|
||||||
|
color:var(--muted);
|
||||||
}
|
}
|
||||||
.status-list{
|
input[type="text"],input[type="number"],textarea,select{
|
||||||
margin:0;
|
width:100%;
|
||||||
padding-left:1.2rem;
|
padding:.6rem;
|
||||||
}
|
border-radius:.4rem;
|
||||||
.status-list li{
|
border:1px solid var(--border);
|
||||||
margin-bottom:.3rem;
|
background:#1f2937;
|
||||||
|
color:var(--text);
|
||||||
|
font:inherit;
|
||||||
|
margin-top:.25rem;
|
||||||
}
|
}
|
||||||
|
textarea{resize:vertical;}
|
||||||
button{
|
button{
|
||||||
padding:.7rem 1rem;
|
margin-top:1rem;
|
||||||
|
padding:.65rem 1.2rem;
|
||||||
|
background:var(--accent-dark);
|
||||||
|
color:#fff;
|
||||||
border:none;
|
border:none;
|
||||||
border-radius:.5rem;
|
border-radius:.5rem;
|
||||||
background:#3b82f6;
|
|
||||||
color:#fff;
|
|
||||||
font-weight:700;
|
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
margin-right:.5rem;
|
font-weight:600;
|
||||||
}
|
}
|
||||||
button.fix{
|
button:hover{background:var(--accent);}
|
||||||
background:#10b981;
|
|
||||||
|
/* Metadata table */
|
||||||
|
.meta-table{
|
||||||
|
width:100%;
|
||||||
|
border-collapse:collapse;
|
||||||
|
margin-top:.5rem;
|
||||||
}
|
}
|
||||||
button.danger{
|
.meta-table td{
|
||||||
background:#dc2626;
|
padding:.5rem;
|
||||||
|
vertical-align:top;
|
||||||
|
border-bottom:1px solid var(--border);
|
||||||
}
|
}
|
||||||
.danger-zone{
|
.meta-table td:first-child{
|
||||||
background:#450a0a;
|
width:30%;
|
||||||
border:1px solid #7f1d1d;
|
font-size:.9rem;
|
||||||
border-radius:.5rem;
|
color:var(--muted);
|
||||||
padding:1rem;
|
padding-left:0;
|
||||||
}
|
}
|
||||||
.danger-zone h2{
|
.meta-table td:last-child{padding-right:0;}
|
||||||
color:#f87171;
|
.meta-table tr:last-child td{border-bottom:none;}
|
||||||
|
.meta-table input,.meta-table textarea{
|
||||||
margin-top:0;
|
margin-top:0;
|
||||||
}
|
}
|
||||||
.path{
|
|
||||||
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
.danger-zone{
|
||||||
font-size:.85rem;
|
grid-column:1 / -1;
|
||||||
word-break:break-all;
|
background:#450a0a;
|
||||||
color:#9ca3af;
|
border:1px solid #7f1d1d;
|
||||||
|
border-radius:.75rem;
|
||||||
|
padding:1.25rem;
|
||||||
}
|
}
|
||||||
|
.danger-zone h2{margin-top:0;color:#fca5a5;}
|
||||||
|
.danger-zone button{
|
||||||
|
background:var(--danger);
|
||||||
|
}
|
||||||
|
.danger-zone button:hover{background:var(--danger-dark);}
|
||||||
|
|
||||||
|
.status-ok{color:var(--success);font-weight:600;}
|
||||||
|
.status-bad{color:var(--warning);font-weight:600;}
|
||||||
|
.copy-hint{font-size:.85rem;color:var(--muted);margin-top:.25rem;}
|
||||||
|
.path{word-break:break-all;font-family:monospace;font-size:.85rem;}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<a href="{{ url_for('admin_dashboard') }}">Dashboard →</a>
|
<h1>Admin Settings</h1>
|
||||||
<a href="{{ url_for('admin_logout') }}">Log out</a>
|
<div class="nav">
|
||||||
|
<a href="{{ url_for('admin_dashboard') }}">← Back to Dashboard</a>
|
||||||
|
<a href="{{ url_for('admin_logout') }}">Log out</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Admin Settings</h1>
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% for category, message in messages %}
|
{% if messages %}
|
||||||
<div class="flash {{ category }}">{{ message }}</div>
|
<div class="messages">
|
||||||
{% endfor %}
|
{% for category, message in messages %}
|
||||||
|
<p class="{{ category }}">{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<!-- Database health -->
|
<div class="grid">
|
||||||
<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 -->
|
<!-- Database health check -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Database Statistics</h2>
|
<h2>Database Health</h2>
|
||||||
<div class="stat-grid">
|
{% if health.ok %}
|
||||||
<div class="stat-card">
|
<p class="status-ok">✅ {{ health.message }}</p>
|
||||||
<div class="value">{{ total_records }}</div>
|
{% else %}
|
||||||
<div class="label">Total requests</div>
|
<p class="status-bad">❌ {{ health.message }}</p>
|
||||||
</div>
|
{% if health.missing_columns %}
|
||||||
<div class="stat-card">
|
<p>Missing columns: {{ health.missing_columns | join(', ') }}</p>
|
||||||
<div class="value">{{ db_size }}</div>
|
<form method="POST" action="{{ url_for('admin_settings') }}">
|
||||||
<div class="label">Database size</div>
|
<input type="hidden" name="action" value="fix_schema">
|
||||||
</div>
|
<button type="submit">Fix Missing Columns</button>
|
||||||
<div class="stat-card">
|
</form>
|
||||||
<div class="value">{{ upload_file_count }}</div>
|
{% endif %}
|
||||||
<div class="label">Uploaded MP3 files</div>
|
{% endif %}
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{% if status_counts %}
|
<!-- Database statistics -->
|
||||||
<h3 style="margin-top:1.2rem;color:#93c5fd">By Status</h3>
|
<div class="section">
|
||||||
<ul class="status-list">
|
<h2>Database Statistics</h2>
|
||||||
{% for key,count in status_counts.items() %}
|
<p><strong>Total requests:</strong> {{ total_records }}</p>
|
||||||
<li>{{ statuses[key] }} ({{ key }}): <strong>{{ count }}</strong></li>
|
<p><strong>Database size:</strong> {{ db_size }}</p>
|
||||||
{% endfor %}
|
<p><strong>Uploaded MP3s:</strong> {{ upload_file_count }} files in {{ upload_dir_count }} request folders</p>
|
||||||
</ul>
|
<p><strong>Uploads total size:</strong> {{ upload_size }}</p>
|
||||||
{% else %}
|
{% if status_counts %}
|
||||||
<p style="margin-top:1rem;color:#9ca3af">No requests yet.</p>
|
<p><strong>Status breakdown:</strong></p>
|
||||||
{% endif %}
|
<ul>
|
||||||
</div>
|
{% for status, count in status_counts.items() %}
|
||||||
|
<li>{{ status }}: {{ count }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<p><strong>Database path:</strong><br><span class="path">{{ db_path }}</span></p>
|
||||||
|
<p><strong>Uploads path:</strong><br><span class="path">{{ upload_path }}</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Paths -->
|
<!-- Revision limit -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Paths</h2>
|
<h2>Revision Limit</h2>
|
||||||
<p><strong>Database:</strong><br><span class="path">{{ db_path }}</span></p>
|
<form method="POST">
|
||||||
<p><strong>Uploads:</strong><br><span class="path">{{ upload_path }}</span></p>
|
<input type="hidden" name="action" value="save_max_revisions">
|
||||||
</div>
|
<label for="max_revisions">Maximum customer revisions allowed</label>
|
||||||
|
<input type="number" id="max_revisions" name="max_revisions" min="0" value="{{ current_max_revisions }}">
|
||||||
|
<p class="copy-hint">Set to 0 to disable customer-submitted revisions. Operators can still upload new versions manually.</p>
|
||||||
|
<button type="submit">Save Revision Limit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Settings controls -->
|
<!-- MP3 metadata defaults -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Booth Settings</h2>
|
<h2>MP3 Metadata Tags</h2>
|
||||||
<form method="POST">
|
<p class="copy-hint">These values are written into every uploaded MP3 file. The song title from the prompt is written to the Title tag automatically. Blank fields are skipped.</p>
|
||||||
<input type="hidden" name="action" value="save_max_revisions">
|
<form method="POST">
|
||||||
<label for="max_revisions">Maximum customer revisions allowed</label>
|
<input type="hidden" name="action" value="save_metadata">
|
||||||
<input type="number" id="max_revisions" name="max_revisions" min="0" value="{{ current_max_revisions }}">
|
<table class="meta-table">
|
||||||
<p class="copy-hint">Set to 0 to disable customer-submitted revisions. Operators can still upload new versions manually.</p>
|
<tr>
|
||||||
<button type="submit">Save Revision Limit</button>
|
<td>Artist</td>
|
||||||
</form>
|
<td><input type="text" id="artist" name="artist" value="{{ metadata.get('artist', '') }}" placeholder="e.g. Trollgorithm"></td>
|
||||||
</div>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Album</td>
|
||||||
|
<td><input type="text" id="album" name="album" value="{{ metadata.get('album', '') }}" placeholder="e.g. Theme Song Booth 2026"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Year</td>
|
||||||
|
<td><input type="text" id="year" name="year" value="{{ metadata.get('year', '') }}" placeholder="e.g. 2026"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Genre</td>
|
||||||
|
<td><input type="text" id="genre" name="genre" value="{{ metadata.get('genre', '') }}" placeholder="e.g. Country Metal"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Comment</td>
|
||||||
|
<td><textarea id="comment" name="comment" rows="3" placeholder="e.g. Custom theme song generated at the booth">{{ metadata.get('comment', '') }}</textarea></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<button type="submit">Save Metadata Defaults</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- MP3 metadata defaults -->
|
<!-- System reset spans both columns -->
|
||||||
<div class="section">
|
<div class="danger-zone">
|
||||||
<h2>MP3 Metadata Tags</h2>
|
<h2>⚠️ Reset System</h2>
|
||||||
<p class="copy-hint">These values are written into every uploaded MP3 file. The song title from the prompt is written to the Title tag automatically.</p>
|
<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">
|
<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="save_metadata">
|
<input type="hidden" name="action" value="reset_system">
|
||||||
|
<button type="submit">Reset System</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="artist">Artist</label>
|
|
||||||
<input type="text" id="artist" name="artist" value="{{ metadata.get('artist', '') }}" placeholder="e.g. Trollgorithm">
|
|
||||||
|
|
||||||
<label for="album">Album</label>
|
|
||||||
<input type="text" id="album" name="album" value="{{ metadata.get('album', '') }}" placeholder="e.g. Theme Song Booth 2026">
|
|
||||||
|
|
||||||
<label for="year">Year</label>
|
|
||||||
<input type="text" id="year" name="year" value="{{ metadata.get('year', '') }}" placeholder="e.g. 2026">
|
|
||||||
|
|
||||||
<label for="genre">Genre</label>
|
|
||||||
<input type="text" id="genre" name="genre" value="{{ metadata.get('genre', '') }}" placeholder="e.g. Country Metal">
|
|
||||||
|
|
||||||
<label for="comment">Comment</label>
|
|
||||||
<textarea id="comment" name="comment" rows="3" placeholder="e.g. Custom theme song generated at the booth">{{ metadata.get('comment', '') }}</textarea>
|
|
||||||
|
|
||||||
<button type="submit">Save Metadata Defaults</button>
|
|
||||||
</form>
|
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Reference in a new issue