Design system refactor: shared CSS tokens, cleaner customer/admin pages, kiosk name privacy, v0.9.1

This commit is contained in:
Troll (Hermes Agent) 2026-08-31 03:17:14 +00:00
parent 2f5050b29f
commit cc27c5db48
10 changed files with 1490 additions and 1224 deletions

View file

@ -4,165 +4,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Settings</title>
<style>
/*
Admin settings / maintenance page.
Two-column layout for booth open/closed status, database health,
statistics, revision limit, auto-refresh, SMTP config,
MP3 metadata defaults, database backup/restore, and system reset.
*/
: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{
margin:0;
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
background:var(--bg);
color:var(--text);
line-height:1.5;
}
.container{max-width:1200px;margin:0 auto;padding:1.5rem;}
.topbar{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:1.5rem;
}
h1{margin:0;font-size:1.5rem;}
.nav a{
color:var(--accent);
text-decoration:none;
margin-left:1rem;
}
.messages p{
padding:.75rem 1rem;
border-radius:.5rem;
margin:0 0 1rem;
}
.messages .success{background:#064e3b;color:#a7f3d0;}
.messages .error{background:#450a0a;color:#fca5a5;}
/* Two-column grid for settings */
.grid{
display:grid;
grid-template-columns:repeat(2, 1fr);
gap:1.5rem;
}
@media (max-width:900px){
.grid{grid-template-columns:1fr;}
}
.section{
background:var(--panel);
border:1px solid var(--border);
border-radius:.75rem;
padding:1.25rem;
}
.section h2{
margin-top:0;
font-size:1.15rem;
color:var(--accent);
}
.section p{
margin:.5rem 0;
color:var(--muted);
font-size:.95rem;
}
label{
display:block;
margin-top:.75rem;
font-size:.9rem;
color:var(--muted);
}
input[type="text"],input[type="number"],input[type="password"],textarea,select{
width:100%;
padding:.6rem;
border-radius:.4rem;
border:1px solid var(--border);
background:#1f2937;
color:var(--text);
font:inherit;
margin-top:.25rem;
}
textarea{resize:vertical;}
button{
margin-top:1rem;
padding:.65rem 1.2rem;
background:var(--accent-dark);
color:#fff;
border:none;
border-radius:.5rem;
cursor:pointer;
font-weight:600;
}
button:hover{background:var(--accent);}
/* Metadata and email config tables */
.meta-table{
width:100%;
border-collapse:collapse;
margin-top:.5rem;
}
.meta-table td{
padding:.5rem;
vertical-align:top;
border-bottom:1px solid var(--border);
}
.meta-table td:first-child{
width:30%;
font-size:.9rem;
color:var(--muted);
padding-left:0;
}
.meta-table td:last-child{padding-right:0;}
.meta-table tr:last-child td{border-bottom:none;}
.meta-table input,.meta-table textarea,.meta-table select{
margin-top:0;
}
.danger-zone{
grid-column:1 / -1;
background:#450a0a;
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;}
.version-tag{
display:inline-block;
background:#2563eb;
color:#fff;
font-size:.85rem;
font-weight:700;
padding:.25rem .6rem;
border-radius:.4rem;
margin-left:.75rem;
vertical-align:middle;
}
</style>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/booth.css') }}">
</head>
<body>
<div class="container">
<body class="booth-page">
<div class="container container-wide">
<div class="topbar">
<h1>Admin Settings <span class="version-tag">v{{ version }}</span></h1>
<div class="nav">
@ -183,7 +31,6 @@
<div class="grid">
<!-- Database health check -->
<div class="section">
<h2>Booth Status</h2>
<p class="copy-hint">When closed, the public request page shows a "booth closed" message instead of the form.</p>
@ -191,14 +38,13 @@
<input type="hidden" name="action" value="save_booth_open">
<label for="booth_open">Booth is currently</label>
<select id="booth_open" name="booth_open">
<option value="1" {% if booth_open %}selected{% endif %}>Open</option>
<option value="0" {% if not booth_open %}selected{% endif %}>Closed</option>
<option value="1" {% if booth_open %}selected{% endif %}>Open</option>
<option value="0" {% if not booth_open %}selected{% endif %}>Closed</option>
</select>
<button type="submit">Save Booth Status</button>
</form>
</div>
<!-- Database health check -->
<div class="section">
<h2>Database Health</h2>
{% if not health.ok %}
@ -215,14 +61,13 @@
</form>
{% else %}
<p class="status-ok">✅ {{ health.message }}</p>
<form method="POST" action="{{ url_for('admin_settings') }}" style="margin-top:.5rem">
<form method="POST" action="{{ url_for('admin_settings') }}" style="margin-top:var(--space-2)">
<input type="hidden" name="action" value="fix_db">
<button type="submit" class="secondary">Recheck / Apply Schema</button>
<button type="submit" class="button-secondary">Recheck / Apply Schema</button>
</form>
{% endif %}
</div>
<!-- Database statistics -->
<div class="section">
<h2>Database Statistics</h2>
<p><strong>Total requests:</strong> {{ total_records }}</p>
@ -241,7 +86,6 @@
<p><strong>Uploads path:</strong><br><span class="path">{{ upload_path }}</span></p>
</div>
<!-- Revision limit -->
<div class="section">
<h2>Revision Limit</h2>
<form method="POST">
@ -253,7 +97,6 @@
</form>
</div>
<!-- Auto refresh -->
<div class="section">
<h2>Dashboard Auto-Refresh</h2>
<form method="POST">
@ -269,7 +112,6 @@
</form>
</div>
<!-- Kiosk cycle -->
<div class="section">
<h2>Kiosk Display Mode</h2>
<p class="copy-hint">Set how the public <a href="{{ url_for('kiosk') }}" target="_blank" rel="noopener noreferrer">/kiosk</a> page cycles. Use -1 for QR only, 0 for pricing only, 1 for queue only, or 5+ seconds to cycle between QR, pricing, and queue.</p>
@ -282,7 +124,6 @@
</form>
</div>
<!-- Hermes API Key display (env var only; no regeneration) -->
<div class="section">
<h2>Hermes API Key</h2>
<p class="copy-hint">Set via Portainer env var <code>HERMES_API_KEY</code>. Used for callback auth.</p>
@ -292,7 +133,7 @@
<p class="status-bad">⚠️ No Hermes API key is configured. Set the HERMES_API_KEY environment variable in Portainer before using the callback workflow.</p>
{% endif %}
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
<hr class="divider">
<h3>Callback Link Expiry</h3>
<p class="copy-hint">How long the signed Hermes callback URL stays valid (in hours). Default is 168 hours (7 days).</p>
@ -304,7 +145,6 @@
</form>
</div>
<!-- ntfy push notifications -->
<div class="section">
<h2>ntfy Notifications</h2>
<p class="copy-hint">Push notifications for new customer requests. Leave either field blank to disable.</p>
@ -330,7 +170,7 @@
<button type="submit">Save ntfy Settings</button>
</form>
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
<hr class="divider">
<h3>Send Test Notification</h3>
<form method="POST">
@ -339,7 +179,6 @@
</form>
</div>
<!-- Hermes webhook integration -->
<div class="section">
<h2>Hermes Webhook</h2>
<p class="copy-hint">Push song request data to Hermes so it can generate a MusicGPT prompt automatically. The URL must include the route name (e.g. https://ntfy.hallsworth.ca/webhooks/trollgorithm-song-info). The secret is encrypted before storage.</p>
@ -361,7 +200,7 @@
<button type="submit">Save Hermes Webhook</button>
</form>
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
<hr class="divider">
<h3>Send Test Request</h3>
<form method="POST">
@ -370,7 +209,6 @@
</form>
</div>
<!-- MusicGPT integration -->
<div class="section">
<h2>MusicGPT Integration</h2>
<p class="copy-hint">API key is set via the <code>MUSICGPT_API_KEY</code> environment variable in Docker.</p>
@ -378,34 +216,33 @@
<p><strong>Webhook URL:</strong> <span class="path">{{ musicgpt_webhook_url }}</span></p>
<p class="copy-hint">This URL is sent as <code>webhook_url</code> with every MusicGPT conversion request. MusicGPT POSTs results here when each task completes.</p>
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
<hr class="divider">
<h3>Album Cover Delivery</h3>
<p class="copy-hint">When enabled, the generated album cover is attached to the delivery email. It is always shown on this admin page.</p>
<form method="POST">
<input type="hidden" name="action" value="save_album_cover">
<label for="deliver_album_cover" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
<input type="checkbox" id="deliver_album_cover" name="deliver_album_cover" value="1" {% if deliver_album_cover %}checked{% endif %} style="width:auto;margin:0">
<label for="deliver_album_cover" class="checkbox-label">
<input type="checkbox" id="deliver_album_cover" name="deliver_album_cover" value="1" {% if deliver_album_cover %}checked{% endif %}>
Attach album cover to delivery email
</label>
<button type="submit">Save Album Cover Setting</button>
</form>
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
<hr class="divider">
<h3>Automatic MusicGPT Polling</h3>
<p class="copy-hint">When enabled, the site automatically polls MusicGPT every few minutes and downloads MP3/WAV files once they are ready.</p>
<form method="POST">
<input type="hidden" name="action" value="save_musicgpt_autopoll">
<label for="musicgpt_autopoll" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
<input type="checkbox" id="musicgpt_autopoll" name="musicgpt_autopoll" value="1" {% if musicgpt_autopoll %}checked{% endif %} style="width:auto;margin:0">
<label for="musicgpt_autopoll" class="checkbox-label">
<input type="checkbox" id="musicgpt_autopoll" name="musicgpt_autopoll" value="1" {% if musicgpt_autopoll %}checked{% endif %}>
Enable automatic MusicGPT polling
</label>
<button type="submit">Save Auto-Poll Setting</button>
</form>
</div>
<!-- Email / SMTP configuration -->
<div class="section">
<h2>Email (SMTP) Settings</h2>
<p class="copy-hint">These settings are used to send confirmation, preview, and delivery emails to customers. The password is stored encrypted using the Flask secret key.</p>
@ -434,12 +271,11 @@
<input type="password" id="smtp_pass" name="smtp_pass" placeholder="{% if email_form.smtp_pass_set %}Stored encrypted — type to replace{% else %}Enter password{% endif %}">
</td>
</tr>
</table>
<button type="submit">Save Email Settings</button>
</form>
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
<hr class="divider">
<h3>Send Test Email</h3>
<form method="POST">
@ -450,7 +286,6 @@
</form>
</div>
<!-- MP3 metadata defaults -->
<div class="section">
<h2>MP3 Metadata Tags</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. Blank fields are skipped.</p>
@ -478,12 +313,11 @@
</form>
</div>
<!-- Database backup / restore -->
<div class="section">
<h2>Database Backup / Restore</h2>
<p class="copy-hint">Download a copy of the SQLite database before the event. Upload a previous backup to restore it; the current database will be renamed as a timestamped backup.</p>
<form method="POST" style="margin-bottom:1rem">
<form method="POST" style="margin-bottom:var(--space-4)">
<input type="hidden" name="action" value="download_db">
<button type="submit">Download Database Backup</button>
</form>
@ -496,7 +330,6 @@
</form>
</div>
<!-- Uploads backup -->
<div class="section">
<h2>Music Files Backup</h2>
<p class="copy-hint">Download all uploaded MP3 and archived revision files as a single ZIP.</p>
@ -506,7 +339,6 @@
</form>
</div>
<!-- System reset spans both columns -->
<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>
@ -515,22 +347,7 @@
<button type="submit">Reset System</button>
</form>
</div>
</div>
</div>
<script>
function copyHermesKey() {
const el = document.getElementById('new-hermes-key');
if (!el) return;
const range = document.createRange();
range.selectNode(el);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
navigator.clipboard.writeText(el.textContent).then(() => {
alert('Hermes API key copied!');
});
}
</script>
</body>
</div>
</body>
</html>