Design system refactor: shared CSS tokens, cleaner customer/admin pages, kiosk name privacy, v0.9.1
This commit is contained in:
parent
2f5050b29f
commit
cc27c5db48
10 changed files with 1490 additions and 1224 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Theme Song Booth (MusicGPT Edition)
|
||||
|
||||

|
||||

|
||||
|
||||
A Flask web application for running a convention booth where visitors request a custom AI-generated theme song. Operators manage the queue from an admin dashboard, generate prompts via Hermes, queue generations through the MusicGPT API, and deliver final songs by email.
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.9.0
|
||||
0.9.1
|
||||
|
|
|
|||
25
app.py
25
app.py
|
|
@ -1069,11 +1069,10 @@ def kiosk():
|
|||
queue = []
|
||||
for r in raw_queue:
|
||||
if r.get('status') in active_statuses:
|
||||
email = r.get('email') or ''
|
||||
name = email.split('@')[0] if '@' in email else email
|
||||
name = (r.get('name') or '').strip()
|
||||
queue.append({
|
||||
'id': r['id'],
|
||||
'name': name or 'Guest',
|
||||
'name': name or f"Guest #{r['id']}",
|
||||
'status': KIOSK_STATUS_MAP[r['status']],
|
||||
'raw_status': r['status'],
|
||||
})
|
||||
|
|
@ -1482,6 +1481,23 @@ def admin_request(rid):
|
|||
steps, current_step = get_request_steps(req)
|
||||
musicgpt_status = req.get('musicgpt_status')
|
||||
|
||||
# MusicGPT progress stepper data
|
||||
mg_steps = [
|
||||
('IN_QUEUE', 'Queued'),
|
||||
('IN_PROGRESS', 'Generating'),
|
||||
('COMPLETED', 'Completed')
|
||||
]
|
||||
mg_index = -1
|
||||
mg_completed = False
|
||||
if musicgpt_status:
|
||||
for i, (key, _) in enumerate(mg_steps):
|
||||
if key == musicgpt_status:
|
||||
mg_index = i
|
||||
if musicgpt_status in ('COMPLETED', 'FINISHED'):
|
||||
mg_completed = True
|
||||
if musicgpt_status in ('FAILED', 'ERROR', 'CANCELLED'):
|
||||
mg_index = -2
|
||||
|
||||
return render_template(
|
||||
'admin/request.html',
|
||||
req=req,
|
||||
|
|
@ -1501,6 +1517,9 @@ def admin_request(rid):
|
|||
steps=steps,
|
||||
current_step=current_step,
|
||||
musicgpt_status=musicgpt_status,
|
||||
mg_steps=mg_steps,
|
||||
mg_index=mg_index,
|
||||
mg_completed=mg_completed,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
1123
static/css/booth.css
Normal file
1123
static/css/booth.css
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -7,119 +7,28 @@
|
|||
{% if refresh_seconds and refresh_seconds > 0 %}
|
||||
<meta http-equiv="refresh" content="{{ refresh_seconds }}">
|
||||
{% endif %}
|
||||
<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') }}">
|
||||
<style>
|
||||
/*
|
||||
Operator dashboard queue.
|
||||
Shows all requests in a table with status filters, per-row Open/Delete
|
||||
actions, a Settings link, auto-refresh hint, and logout.
|
||||
*/
|
||||
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:1100px;margin:0 auto;}
|
||||
h1{color:#60a5fa;margin-top:0;}
|
||||
.filters{margin-bottom:1rem;display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;}
|
||||
.filters a{
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
padding:.35rem .7rem;
|
||||
border-radius:.4rem;
|
||||
border:1px solid transparent;
|
||||
}
|
||||
.filters a:hover{background:#1f2937;}
|
||||
.filters a.active{
|
||||
font-weight:bold;
|
||||
color:#fff;
|
||||
background:#2563eb;
|
||||
border-color:#2563eb;
|
||||
}
|
||||
table{
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
background:#1f2937;
|
||||
border-radius:.5rem;
|
||||
overflow:hidden;
|
||||
}
|
||||
th,td{padding:.7rem;text-align:left;border-bottom:1px solid #374151;}
|
||||
th{background:#111827;color:#9ca3af;}
|
||||
tr:hover{background:#2d3748;}
|
||||
.status-badge{
|
||||
display:inline-block;
|
||||
padding:.25rem .6rem;
|
||||
border-radius:9999px;
|
||||
font-size:.8rem;
|
||||
font-weight:600;
|
||||
background:#374151;
|
||||
}
|
||||
.awaiting_payment{background:#f59e0b;color:#000;}
|
||||
.paid,.delivered{background:#10b981;color:#000;}
|
||||
.pending,.prompt_ready{background:#60a5fa;color:#000;}
|
||||
.songs_uploaded{background:#a78bfa;color:#000;}
|
||||
.revisions_requested{background:#f87171;color:#000;}
|
||||
.actions a{
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
margin-right:.8rem;
|
||||
}
|
||||
.actions button.delete{
|
||||
background:transparent;
|
||||
color:#f87171;
|
||||
border:none;
|
||||
padding:0;
|
||||
cursor:pointer;
|
||||
font:inherit;
|
||||
text-decoration:none;
|
||||
margin-right:.8rem;
|
||||
}
|
||||
.logout{float:right;color:#f87171;text-decoration:none;}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
|
||||
/* Topbar with Settings and Log out link */
|
||||
.topbar{
|
||||
float:right;
|
||||
display:flex;
|
||||
gap:.75rem;
|
||||
align-items:center;
|
||||
}
|
||||
.topbar form{display:inline;}
|
||||
.topbar a{
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
}
|
||||
.topbar a.settings{
|
||||
color:#10b981;
|
||||
}
|
||||
|
||||
.refresh-hint{
|
||||
color:#6b7280;
|
||||
font-size:.85rem;
|
||||
margin-left:auto;
|
||||
}
|
||||
.dashboard-table td { white-space: nowrap; }
|
||||
.dashboard-table td.wrap { white-space: normal; max-width: 220px; }
|
||||
.dashboard-table .id-cell { font-family: var(--font-mono); font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Topbar: Kiosk, Pricing, Sales, Settings, Log out -->
|
||||
<body class="booth-page">
|
||||
<div class="container container-wide">
|
||||
<div class="topbar">
|
||||
<h1>Theme Song Booth — Admin Dashboard</h1>
|
||||
<div class="nav">
|
||||
<a href="{{ url_for('kiosk') }}" target="_blank" rel="noopener noreferrer">Kiosk</a>
|
||||
<a href="{{ url_for('admin_pricing') }}">Pricing</a>
|
||||
<a href="{{ url_for('admin_sales') }}">Sales</a>
|
||||
<a href="{{ url_for('admin_settings') }}" class="settings">Settings</a>
|
||||
<a href="{{ url_for('admin_logout') }}" class="logout">Log out</a>
|
||||
<a href="{{ url_for('admin_settings') }}" class="text-success" style="font-weight:700">Settings</a>
|
||||
<a href="{{ url_for('admin_logout') }}" class="text-danger">Log out</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Theme Song Booth — Admin Dashboard</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
|
|
@ -127,7 +36,6 @@
|
|||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Status filter links -->
|
||||
<div class="filters">
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="{% if current_status in (None, '') %}active{% endif %}">All</a>
|
||||
<a href="{{ url_for('admin_dashboard', status='pending') }}" class="{% if current_status == 'pending' %}active{% endif %}">Pending</a>
|
||||
|
|
@ -135,7 +43,7 @@
|
|||
<a href="{{ url_for('admin_dashboard', status='awaiting_payment') }}" class="{% if current_status == 'awaiting_payment' %}active{% endif %}">Awaiting Payment</a>
|
||||
<a href="{{ url_for('admin_dashboard', status='delivered') }}" class="{% if current_status == 'delivered' %}active{% endif %}">Delivered</a>
|
||||
<form method="POST" action="{{ url_for('admin_musicgpt_refresh') }}" style="display:inline;margin-left:auto">
|
||||
<button type="submit" class="secondary" style="margin:0;padding:.35rem .7rem;font-weight:600;background:#4b5563">Refresh MusicGPT Status</button>
|
||||
<button type="submit" class="button-secondary" style="padding:var(--space-1) var(--space-3);font-size:0.85rem">Refresh MusicGPT Status</button>
|
||||
</form>
|
||||
<span class="refresh-hint">
|
||||
{% if refresh_seconds and refresh_seconds > 0 %}
|
||||
|
|
@ -146,8 +54,8 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Requests table -->
|
||||
<table>
|
||||
<div class="table-wrap">
|
||||
<table class="dashboard-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
|
@ -162,25 +70,44 @@
|
|||
<tbody>
|
||||
{% for r in requests %}
|
||||
<tr>
|
||||
<td>#{{ r.id }}</td>
|
||||
<td>{{ r.name }}</td>
|
||||
<td>{{ r.email }}</td>
|
||||
<td>{{ r.style_genre or '-' }}</td>
|
||||
<td class="id-cell">#{{ r.id }}</td>
|
||||
<td class="wrap">{{ r.name }}</td>
|
||||
<td class="wrap">{{ r.email }}</td>
|
||||
<td class="wrap">{{ r.style_genre or '-' }}</td>
|
||||
<td><span class="status-badge {{ r.status }}">{{ statuses[r.status] }}</span></td>
|
||||
<td>{% if r.customer_approved and r.customer_approved != 'none' %}{{ (r.customer_approved or 'none').upper() }}{% else %}-{% endif %}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ url_for('admin_request', rid=r.id) }}">Open</a>
|
||||
<td>{% if r.customer_approved and r.customer_approved != 'none' %}{{ (r.customer_approved or 'none').upper() }}{% else %}-<{% endif %}</td>
|
||||
<td class="table-actions">
|
||||
<a href="{{ url_for('admin_request', rid=r.id) }}" class="button">Open</a>
|
||||
{% if r.status in ('pending', 'revisions_requested') %}
|
||||
<a href="{{ url_for('admin_request', rid=r.id) }}#prompt" class="button-secondary">Prompt</a>
|
||||
{% endif %}
|
||||
{% if r.status == 'prompt_ready' and not r.musicgpt_status %}
|
||||
<form method="POST" action="{{ url_for('admin_request', rid=r.id) }}" style="display:inline">
|
||||
<input type="hidden" name="action" value="generate_musicgpt">
|
||||
<button type="submit" class="button-secondary" style="font-size:0.8rem">Generate</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if r.status == 'songs_uploaded' and r.customer_approved == 'none' %}
|
||||
<form method="POST" action="{{ url_for('admin_request', rid=r.id) }}" style="display:inline">
|
||||
<input type="hidden" name="action" value="notify_customer">
|
||||
<button type="submit" class="button-secondary" style="font-size:0.8rem">Send Preview</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if r.status == 'awaiting_payment' %}
|
||||
<a href="{{ url_for('admin_request', rid=r.id) }}#delivery" class="button-secondary">Deliver</a>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('admin_delete_request', rid=r.id) }}" style="display:inline" onsubmit="return confirm('ARE YOU SURE you want to delete request #{{ r.id }} for {{ r.name }}? This cannot be undone.')">
|
||||
<button type="submit" class="delete">Delete</button>
|
||||
<button type="submit" class="button-danger" style="font-size:0.8rem">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not requests %}
|
||||
<tr><td colspan="7">No requests found.</td></tr>
|
||||
<tr><td colspan="7" style="text-align:center;color:var(--text-muted);padding:var(--space-8)">No requests found.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -4,255 +4,40 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request #{{ req.id }} — Admin</title>
|
||||
<style>
|
||||
/*
|
||||
Operator detail page for a single request.
|
||||
Two-column layout:
|
||||
left -> customer info + internal operator notes
|
||||
right -> Suno prompt, song upload, customer notification,
|
||||
payment reference, and file delivery.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
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:1200px;margin:0 auto;}
|
||||
h1,h2{color:#60a5fa;margin-top:0;}
|
||||
a{color:#93c5fd;}
|
||||
.section{
|
||||
background:#1f2937;
|
||||
padding:1rem;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
label{display:block;margin-top:.8rem;font-weight:600;}
|
||||
input,textarea{
|
||||
width:100%;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
box-sizing:border-box;
|
||||
font:inherit;
|
||||
}
|
||||
textarea{min-height:100px;}
|
||||
button{
|
||||
padding:.7rem 1rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:#3b82f6;
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
margin-top:.5rem;
|
||||
}
|
||||
button.secondary{background:#4b5563;}
|
||||
button.danger{background:#dc2626;}
|
||||
button.success{background:#10b981;}
|
||||
button:disabled{background:#374151;color:#9ca3af;cursor:not-allowed;}
|
||||
.actions{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem;}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
.status-badge{
|
||||
display:inline-block;
|
||||
padding:.25rem .6rem;
|
||||
border-radius:9999px;
|
||||
font-size:.8rem;
|
||||
font-weight:600;
|
||||
background:#374151;
|
||||
color:#f3f4f6;
|
||||
margin-right:.3rem;
|
||||
}
|
||||
.status-badge.ok{background:#10b981;color:#000;}
|
||||
.status-badge.missing{background:#ef4444;color:#000;}
|
||||
.status-badge.sent{background:#60a5fa;color:#000;}
|
||||
.copy-hint{font-size:.85rem;color:#9ca3af;margin-top:.3rem;}
|
||||
.revision-note{
|
||||
margin-top:1rem;
|
||||
background:#450a0a;
|
||||
border:1px solid #7f1d1d;
|
||||
border-radius:.5rem;
|
||||
padding:1rem;
|
||||
}
|
||||
.revision-note h3{
|
||||
margin-top:0;
|
||||
color:#f87171;
|
||||
}
|
||||
.file-select{
|
||||
background:#111827;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
margin:.3rem 0;
|
||||
}
|
||||
.file-select input{
|
||||
width:auto;
|
||||
margin-right:.5rem;
|
||||
}
|
||||
.file-select label{
|
||||
display:inline;
|
||||
margin:0;
|
||||
font-weight:400;
|
||||
}
|
||||
.song-row{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
gap:.75rem;
|
||||
margin:.6rem 0;
|
||||
padding:.4rem 0;
|
||||
}
|
||||
.song-row input[type="checkbox"],
|
||||
.song-row input[type="radio"]{
|
||||
width:auto;
|
||||
margin:.2rem 0 0 0;
|
||||
flex-shrink:0;
|
||||
}
|
||||
.song-row label{
|
||||
display:block;
|
||||
margin:0;
|
||||
font-weight:400;
|
||||
line-height:1.4;
|
||||
}
|
||||
.old-rev{
|
||||
font-size:.85rem;
|
||||
color:#9ca3af;
|
||||
margin-left:1.8rem;
|
||||
}
|
||||
.approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24;}
|
||||
.status-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;}
|
||||
.media-status{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
gap:1rem;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.cover-col{flex-shrink:0;}
|
||||
.status-col{flex:1;min-width:200px;}
|
||||
.steps{
|
||||
display:flex;
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
gap:.25rem;
|
||||
align-items:flex-start;
|
||||
}
|
||||
.step-item{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
text-align:center;
|
||||
flex:1;
|
||||
min-width:80px;
|
||||
position:relative;
|
||||
}
|
||||
.step-item:not(:last-child)::after{
|
||||
content:"";
|
||||
position:absolute;
|
||||
top:1rem;
|
||||
left:50%;
|
||||
width:100%;
|
||||
height:2px;
|
||||
background:#4b5563;
|
||||
z-index:0;
|
||||
}
|
||||
.step-item.completed:not(:last-child)::after{
|
||||
background:#10b981;
|
||||
}
|
||||
.step-indicator{
|
||||
width:2rem;
|
||||
height:2rem;
|
||||
border-radius:50%;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
font-size:.9rem;
|
||||
font-weight:700;
|
||||
background:#374151;
|
||||
color:#f3f4f6;
|
||||
position:relative;
|
||||
z-index:1;
|
||||
border:2px solid transparent;
|
||||
}
|
||||
.step-item.completed .step-indicator{
|
||||
background:#10b981;
|
||||
color:#000;
|
||||
}
|
||||
.step-item.current .step-indicator{
|
||||
background:#3b82f6;
|
||||
color:#fff;
|
||||
border-color:#93c5fd;
|
||||
}
|
||||
.step-item.upcoming .step-indicator{
|
||||
background:#374151;
|
||||
color:#9ca3af;
|
||||
}
|
||||
.step-label{
|
||||
font-size:.75rem;
|
||||
margin-top:.35rem;
|
||||
color:#f3f4f6;
|
||||
}
|
||||
.step-item.upcoming .step-label{
|
||||
color:#9ca3af;
|
||||
}
|
||||
.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{
|
||||
display:grid;
|
||||
grid-template-columns:1fr 1fr;
|
||||
gap:1rem;
|
||||
}
|
||||
@media(max-width:900px){
|
||||
.two-col{grid-template-columns:1fr;}
|
||||
}
|
||||
</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">
|
||||
<p><a href="{{ url_for('admin_dashboard') }}">← Dashboard</a></p>
|
||||
<body class="booth-page">
|
||||
<div class="container container-wide">
|
||||
<p class="mb-2"><a href="{{ url_for('admin_dashboard') }}">← Dashboard</a></p>
|
||||
<h1>Request #{{ req.id }} — {{ req.name }}</h1>
|
||||
|
||||
<!-- Album cover + status info panel -->
|
||||
<div class="section">
|
||||
<div class="section-nav">
|
||||
<a href="#customer">Customer</a>
|
||||
<a href="#prompt">Prompt</a>
|
||||
<a href="#songs">Songs</a>
|
||||
<a href="#notify">Notify</a>
|
||||
<a href="#delivery">Payment & Delivery</a>
|
||||
<a href="#notes">Notes</a>
|
||||
</div>
|
||||
|
||||
<!-- Status panel -->
|
||||
<div class="section" id="status">
|
||||
<div class="media-status">
|
||||
{% if req.album_cover_url %}
|
||||
<div class="cover-col">
|
||||
<img src="{{ req.album_cover_url }}" alt="Album cover" style="max-width:160px;border-radius:.5rem;display:block;">
|
||||
<img src="{{ req.album_cover_url }}" alt="Album cover">
|
||||
{% if not deliver_album_cover %}
|
||||
<p class="copy-hint" style="margin:.25rem 0 0 0;max-width:160px;">Album cover is hidden from the customer preview/delivery because delivery is disabled in settings.</p>
|
||||
<p class="cover-hint">Album cover is hidden from the customer preview/delivery because delivery is disabled in settings.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="status-col">
|
||||
<p style="margin:0;font-size:.8rem;color:#9ca3af;text-transform:uppercase;letter-spacing:.05em">Request Status</p>
|
||||
<p class="text-muted" style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;margin:0">Request Status</p>
|
||||
{% if current_step == -1 %}
|
||||
<p style="margin:.25rem 0 0 0"><span class="status-badge missing">Cancelled</span></p>
|
||||
<p class="mt-2"><span class="status-badge cancelled">Cancelled</span></p>
|
||||
{% else %}
|
||||
<ol class="steps" aria-label="Request progress">
|
||||
{% for i in range(steps|length) %}
|
||||
|
|
@ -261,9 +46,7 @@
|
|||
{% set is_completed = i < current_step or (is_last and current_step == i) %}
|
||||
{% set is_current = i == current_step and not is_last %}
|
||||
<li class="step-item {% if is_completed %}completed{% elif is_current %}current{% else %}upcoming{% endif %}" {% if is_current %}aria-current="step"{% endif %}>
|
||||
<div class="step-indicator">
|
||||
{% if is_completed %}✓{% else %}{{ i + 1 }}{% endif %}
|
||||
</div>
|
||||
<div class="step-indicator">{% if is_completed %}✓{% else %}{{ i + 1 }}{% endif %}</div>
|
||||
<div class="step-label">{{ label }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
@ -271,47 +54,33 @@
|
|||
{% endif %}
|
||||
|
||||
{% if req.musicgpt_status %}
|
||||
<p style="margin:1rem 0 0 0;font-size:.8rem;color:#9ca3af;text-transform:uppercase;letter-spacing:.05em">Recording Studio Status</p>
|
||||
<p class="text-muted" style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;margin-top:var(--space-4)">Recording Studio Status</p>
|
||||
<ol class="steps" aria-label="MusicGPT progress">
|
||||
{% set mg_steps = [
|
||||
('IN_QUEUE', 'Queued'),
|
||||
('IN_PROGRESS', 'Generating'),
|
||||
('COMPLETED', 'Completed')
|
||||
] %}
|
||||
{% set current_mg = req.musicgpt_status %}
|
||||
{% set ns = namespace(mg_index=-1) %}
|
||||
{% for j in range(mg_steps|length) %}
|
||||
{% if mg_steps[j][0] == current_mg %}{% set ns.mg_index = j %}{% endif %}
|
||||
{% endfor %}
|
||||
{% if current_mg in ('FAILED', 'ERROR', 'CANCELLED') %}{% set ns.mg_index = -2 %}{% endif %}
|
||||
{% set mg_completed_step = req.musicgpt_status in ('COMPLETED', 'FINISHED') %}
|
||||
{% for j in range(mg_steps|length) %}
|
||||
{% set mg_key, mg_label = mg_steps[j] %}
|
||||
{% set mg_completed = j < ns.mg_index or mg_completed_step %}
|
||||
{% set mg_current = j == ns.mg_index and not mg_completed_step %}
|
||||
<li class="step-item {% if mg_completed %}completed{% elif mg_current %}current{% else %}upcoming{% endif %} {% if ns.mg_index == -2 and not mg_completed and not mg_current %}upcoming{% endif %}" {% if mg_current %}aria-current="step"{% endif %}>
|
||||
<div class="step-indicator">
|
||||
{% if mg_completed %}✓{% else %}{{ j + 1 }}{% endif %}
|
||||
</div>
|
||||
<li class="step-item {% if mg_index > j or mg_completed %}completed{% elif mg_index == j %}current{% else %}upcoming{% endif %}" {% if mg_index == j %}aria-current="step"{% endif %}>
|
||||
<div class="step-indicator">{% if mg_index > j or mg_completed %}✓{% else %}{{ j + 1 }}{% endif %}</div>
|
||||
<div class="step-label">{{ mg_label }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
||||
{% set total_cost = (req.musicgpt_cost or 0) + (req.stems_cost or 0) %}
|
||||
{% if total_cost > 0 %}
|
||||
<p style="margin:.5rem 0 0 0"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(total_cost) }}
|
||||
<p class="mt-2"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(total_cost) }}
|
||||
{% if req.stems_cost %}
|
||||
<span style="font-size:.75rem;color:#9ca3af"> (songs: {{ format_musicgpt_cost(req.musicgpt_cost) }} + stems: {{ format_musicgpt_cost(req.stems_cost) }})</span>
|
||||
<span class="text-muted" style="font-size:0.75rem"> (songs: {{ format_musicgpt_cost(req.musicgpt_cost) }} + stems: {{ format_musicgpt_cost(req.stems_cost) }})</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% elif req.musicgpt_cost is not none %}
|
||||
<p style="margin:.5rem 0 0 0"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(req.musicgpt_cost) }}</p>
|
||||
<p class="mt-2"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(req.musicgpt_cost) }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if req.status != 'cancelled' %}
|
||||
<form method="POST" onsubmit="return confirm('Are you sure you want to cancel request #{{ req.id }}?')" style="margin:.75rem 0 0 0">
|
||||
<form method="POST" onsubmit="return confirm('Are you sure you want to cancel request #{{ req.id }}?')" class="mt-4">
|
||||
<input type="hidden" name="action" value="cancel_request">
|
||||
<button type="submit" class="danger" style="margin:0">Cancel Request</button>
|
||||
<button type="submit" class="button-danger">Cancel Request</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -327,8 +96,7 @@
|
|||
<div class="two-col">
|
||||
<!-- LEFT COLUMN -->
|
||||
<div class="left">
|
||||
<!-- Customer Info -->
|
||||
<div class="section">
|
||||
<div class="section" id="customer">
|
||||
<h2>Customer Info</h2>
|
||||
<p class="copy-hint">Operators can correct customer details here. Click Save when done.</p>
|
||||
<form method="POST">
|
||||
|
|
@ -340,7 +108,7 @@
|
|||
<label for="customer_name">Name / persona</label>
|
||||
<input type="text" id="customer_name" name="name" value="{{ req.name }}" required>
|
||||
|
||||
<label for="customer_pronouns">Pronouns <span style="color:#f87171">*</span></label>
|
||||
<label for="customer_pronouns">Pronouns <span class="required">*</span></label>
|
||||
<select id="customer_pronouns" name="pronouns" required>
|
||||
<option value="" {% if not req.pronouns %}selected{% endif %}>— Select pronouns —</option>
|
||||
<option value="He/Him/His" {% if req.pronouns == 'He/Him/His' %}selected{% endif %}>He/Him/His</option>
|
||||
|
|
@ -354,7 +122,7 @@
|
|||
<label for="customer_notable_facts">Notable things</label>
|
||||
<textarea id="customer_notable_facts" name="notable_facts" rows="3">{{ req.notable_facts or '' }}</textarea>
|
||||
|
||||
<label for="customer_style_genre_decade">Decade / era <span style="color:#f87171">*</span></label>
|
||||
<label for="customer_style_genre_decade">Decade / era <span class="required">*</span></label>
|
||||
<select id="customer_style_genre_decade" name="decade" required>
|
||||
<option value="" {% if not style_parts.decade %}selected{% endif %}>— Select decade —</option>
|
||||
{% for d in decades %}
|
||||
|
|
@ -362,7 +130,7 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="customer_style_genre_basic">Basic style <span style="color:#f87171">*</span></label>
|
||||
<label for="customer_style_genre_basic">Basic style <span class="required">*</span></label>
|
||||
<select id="customer_style_genre_basic" name="basic_style" required>
|
||||
<option value="" {% if not style_parts.basic_style %}selected{% endif %}>— Select style —</option>
|
||||
{% for g in genres %}
|
||||
|
|
@ -378,7 +146,7 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Stored style: {{ req.style_genre or '—' }}</p>
|
||||
<p class="copy-hint">Stored style: {{ req.style_genre or '—' }}</p>
|
||||
|
||||
<label for="customer_vocal_gender">Preferred singer voice / gender</label>
|
||||
<select id="customer_vocal_gender" name="vocal_gender">
|
||||
|
|
@ -393,29 +161,28 @@
|
|||
<label for="customer_extra_requests">Anything else</label>
|
||||
<textarea id="customer_extra_requests" name="extra_requests" rows="3" maxlength="2000">{{ req.extra_requests or '' }}</textarea>
|
||||
|
||||
<label for="customer_stems_interest" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
|
||||
<input type="checkbox" id="customer_stems_interest" name="stems_interest" value="1" {% if req.stems_interest %}checked{% endif %} style="width:auto;margin:0">
|
||||
<label for="customer_stems_interest" class="checkbox-label">
|
||||
<input type="checkbox" id="customer_stems_interest" name="stems_interest" value="1" {% if req.stems_interest %}checked{% endif %}>
|
||||
Customer is interested in STEMS / multitrack files
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="secondary">Save Customer Info</button>
|
||||
<button type="submit" class="button-secondary">Save Customer Info</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p style="margin-top:1rem"><strong>Request #:</strong> {{ req.id }}</p>
|
||||
<p class="mt-4 mb-0"><strong>Request #:</strong> {{ req.id }}</p>
|
||||
|
||||
{% if req.revision_note %}
|
||||
<div class="revision-note">
|
||||
<h3>📝 Revisions Requested{% if req.revision_count %}<span style="float:right">Revision #{{ req.revision_count }}</span>{% endif %}</h3>
|
||||
<h3>Revisions Requested{% if req.revision_count %}<span style="float:right">Revision #{{ req.revision_count }}</span>{% endif %}</h3>
|
||||
<p>{{ req.revision_note }}</p>
|
||||
<button type="button" onclick="copyRevisionPromptForHermes()">📋 Copy revision prompt for Hermes</button>
|
||||
<button type="button" onclick="copyRevisionPromptForHermes()" class="button-secondary">Copy revision prompt for Hermes</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Revision History -->
|
||||
<div class="section">
|
||||
<div class="section" id="history">
|
||||
<h2>Revision History</h2>
|
||||
{% if revision_history %}
|
||||
<ul class="history-list">
|
||||
|
|
@ -437,8 +204,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Operator Notes -->
|
||||
<div class="section">
|
||||
<div class="section" id="notes">
|
||||
<h2>Operator Notes</h2>
|
||||
<p class="copy-hint">Internal notes for the booth team. Customers cannot see this.</p>
|
||||
<form method="POST">
|
||||
|
|
@ -446,13 +212,12 @@
|
|||
<label for="operator_notes">Notes</label>
|
||||
<textarea id="operator_notes" name="operator_notes" rows="4" placeholder="e.g. paid cash, VIP friend, follow up...">{{ req.operator_notes or '' }}</textarea>
|
||||
<div class="actions">
|
||||
<button type="submit" class="secondary">Save Notes</button>
|
||||
<button type="submit" class="button-secondary">Save Notes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Stems / Extras Link -->
|
||||
<div class="section">
|
||||
<div class="section" id="stems">
|
||||
<h2>Stems / Extras Link</h2>
|
||||
<p class="copy-hint">Auto-generated from Gokapi when stems are completed. You can also paste a custom share link here — it will appear in the delivery email.</p>
|
||||
<form method="POST">
|
||||
|
|
@ -460,9 +225,9 @@
|
|||
<label for="stems_link">Share link</label>
|
||||
<input type="url" id="stems_link" name="stems_link" value="{{ req.stems_link or '' }}" placeholder="https://files.dionysismedia.ca/d?id=...">
|
||||
<div class="actions">
|
||||
<button type="submit" class="secondary">Save Link</button>
|
||||
<button type="submit" class="button-secondary">Save Link</button>
|
||||
{% if req.stems_url and not req.stems_link %}
|
||||
<button type="submit" name="action" value="reprocess_stems" class="secondary">Re-upload to Gokapi</button>
|
||||
<button type="submit" name="action" value="reprocess_stems" class="button-secondary">Re-upload to Gokapi</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -471,12 +236,11 @@
|
|||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div class="right">
|
||||
<!-- Generate Music Prompt -->
|
||||
<div class="section">
|
||||
<div class="section" id="prompt">
|
||||
<h2>1. Generate & Save Music Prompt</h2>
|
||||
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
|
||||
<button type="button" onclick="copyPromptForHermes()" class="button-secondary">Copy customer info for Hermes</button>
|
||||
<form method="POST" action="{{ url_for('send_to_hermes', rid=req.id) }}" style="display:inline">
|
||||
<button type="submit" onclick="return confirm('Send this request to Hermes to generate a MusicGPT prompt?')">🚀 Send to Hermes</button>
|
||||
<button type="submit" onclick="return confirm('Send this request to Hermes to generate a MusicGPT prompt?')">Send to Hermes</button>
|
||||
</form>
|
||||
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save or generate.</p>
|
||||
|
||||
|
|
@ -493,14 +257,14 @@
|
|||
<label for="suno_lyrics">Lyrics (with metatags)</label>
|
||||
<textarea id="suno_lyrics" name="suno_lyrics" rows="10">{{ req.suno_lyrics or '' }}</textarea>
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_lyrics', 'Lyrics')">Copy Lyrics</button>
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_style', 'Style')">Copy Style</button>
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_title', 'Title')">Copy Title</button>
|
||||
<button type="button" class="button-secondary" onclick="copyToClipboard('suno_lyrics', 'Lyrics')">Copy Lyrics</button>
|
||||
<button type="button" class="button-secondary" onclick="copyToClipboard('suno_style', 'Style')">Copy Style</button>
|
||||
<button type="button" class="button-secondary" onclick="copyToClipboard('suno_title', 'Title')">Copy Title</button>
|
||||
<button type="submit">Save Prompt</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr style="border-color:#374151;margin:1rem 0">
|
||||
<hr class="divider">
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="generate_musicgpt">
|
||||
|
|
@ -515,8 +279,8 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="deliver_wav_gen" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
|
||||
<input type="checkbox" id="deliver_wav_gen" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %} style="width:auto;margin:0">
|
||||
<label for="deliver_wav_gen" class="checkbox-label">
|
||||
<input type="checkbox" id="deliver_wav_gen" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %}>
|
||||
Include WAV files with delivery
|
||||
</label>
|
||||
|
||||
|
|
@ -524,54 +288,51 @@
|
|||
<div class="actions">
|
||||
<button type="submit" {% if in_progress %}disabled{% endif %}>Generate A/B with MusicGPT</button>
|
||||
{% if in_progress %}
|
||||
<button type="submit" name="action" value="cancel_musicgpt" class="danger">Cancel Generation</button>
|
||||
<button type="submit" name="action" value="cancel_musicgpt" class="button-danger">Cancel Generation</button>
|
||||
{% endif %}
|
||||
{% if req.musicgpt_task_id and req.musicgpt_status not in ('IN_QUEUE', 'IN_PROGRESS') %}
|
||||
<button type="submit" formaction="{{ url_for('admin_musicgpt_poll_request', rid=req.id) }}" class="secondary">Poll MusicGPT</button>
|
||||
<button type="submit" formaction="{{ url_for('admin_musicgpt_poll_request', rid=req.id) }}" class="button-secondary">Poll MusicGPT</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Upload Songs -->
|
||||
<div class="section">
|
||||
<div class="section" id="songs">
|
||||
<h2>2. Songs</h2>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="delete_songs">
|
||||
<div class="song-row">
|
||||
<input type="checkbox" id="delete_a" name="delete_song" value="a"
|
||||
{% if not (req.song_a_path and file_exists(req.song_a_path)) %}disabled{% endif %}>
|
||||
<input type="checkbox" id="delete_a" name="delete_song" value="a" {% if not (req.song_a_path and file_exists(req.song_a_path)) %}disabled{% endif %}>
|
||||
<label for="delete_a">Version A MP3:
|
||||
{% if req.song_a_path and file_exists(req.song_a_path) %}
|
||||
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_a_path) }}
|
||||
<span class="status-badge ok">Ready</span> {{ basename(req.song_a_path) }}
|
||||
{% if req.song_a_wav_path and file_exists(req.song_a_wav_path) %}
|
||||
<br><span class="status-badge ok">WAV</span> {{ basename(req.song_a_wav_path) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not ready</span>
|
||||
<span class="status-badge missing">Not ready</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="song-row">
|
||||
<input type="checkbox" id="delete_b" name="delete_song" value="b"
|
||||
{% if not (req.song_b_path and file_exists(req.song_b_path)) %}disabled{% endif %}>
|
||||
<input type="checkbox" id="delete_b" name="delete_song" value="b" {% if not (req.song_b_path and file_exists(req.song_b_path)) %}disabled{% endif %}>
|
||||
<label for="delete_b">Version B MP3:
|
||||
{% if req.song_b_path and file_exists(req.song_b_path) %}
|
||||
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_b_path) }}
|
||||
<span class="status-badge ok">Ready</span> {{ basename(req.song_b_path) }}
|
||||
{% if req.song_b_wav_path and file_exists(req.song_b_wav_path) %}
|
||||
<br><span class="status-badge ok">WAV</span> {{ basename(req.song_b_wav_path) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not ready</span>
|
||||
<span class="status-badge missing">Not ready</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" class="danger" onclick="return confirm('Delete selected song files? This will reset the request to Prompt Ready.')">Delete Selected Songs</button>
|
||||
<button type="submit" class="button-danger" onclick="return confirm('Delete selected song files? This will reset the request to Prompt Ready.')">Delete Selected Songs</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr style="border-color:#374151;margin:1rem 0">
|
||||
<hr class="divider">
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" value="upload_songs">
|
||||
|
|
@ -584,44 +345,37 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<hr style="border-color:#374151;margin:1rem 0">
|
||||
<hr class="divider">
|
||||
|
||||
<!-- Stems -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="generate_stems">
|
||||
<p style="margin:0 0 .5rem 0;font-weight:600">Select source MP3 for stems extraction:</p>
|
||||
<p style="margin:0 0 var(--space-2) 0;font-weight:600">Select source MP3 for stems extraction:</p>
|
||||
{% set a_ready = req.song_a_path and file_exists(req.song_a_path) %}
|
||||
{% set b_ready = req.song_b_path and file_exists(req.song_b_path) %}
|
||||
<div class="song-row">
|
||||
<input type="radio" id="stems_a" name="stems_source" value="a"
|
||||
{% if not a_ready %}disabled{% endif %}
|
||||
{% if a_ready and not b_ready %}checked{% endif %}>
|
||||
<label for="stems_a" {% if not a_ready %}style="opacity:.6"{% endif %}>
|
||||
Version A MP3:
|
||||
<input type="radio" id="stems_a" name="stems_source" value="a" {% if not a_ready %}disabled{% endif %} {% if a_ready and not b_ready %}checked{% endif %}>
|
||||
<label for="stems_a" {% if not a_ready %}style="opacity:.6"{% endif %}>Version A MP3:
|
||||
{% if a_ready %}
|
||||
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_a_path) }}
|
||||
<span class="status-badge ok">Ready</span> {{ basename(req.song_a_path) }}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not ready</span>
|
||||
<span class="status-badge missing">Not ready</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="song-row">
|
||||
<input type="radio" id="stems_b" name="stems_source" value="b"
|
||||
{% if not b_ready %}disabled{% endif %}
|
||||
{% if b_ready %}checked{% endif %}>
|
||||
<label for="stems_b" {% if not b_ready %}style="opacity:.6"{% endif %}>
|
||||
Version B MP3:
|
||||
<input type="radio" id="stems_b" name="stems_source" value="b" {% if not b_ready %}disabled{% endif %} {% if b_ready %}checked{% endif %}>
|
||||
<label for="stems_b" {% if not b_ready %}style="opacity:.6"{% endif %}>Version B MP3:
|
||||
{% if b_ready %}
|
||||
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_b_path) }}
|
||||
<span class="status-badge ok">Ready</span> {{ basename(req.song_b_path) }}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not ready</span>
|
||||
<span class="status-badge missing">Not ready</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<p class="copy-hint">Vocal + instrumental extraction: ~$0.018–$0.084 USD per song on Pro plan.</p>
|
||||
|
||||
<label for="stems_status">Stems Status:</label>
|
||||
<p>
|
||||
<p class="mt-0">
|
||||
{% if req.stems_status %}
|
||||
<span class="status-badge {% if req.stems_status == 'COMPLETED' %}ok{% elif req.stems_status in ('FAILED','ERROR') %}missing{% endif %}">{{ req.stems_status }}</span>
|
||||
{% if req.stems_cost is not none %}({{ format_musicgpt_cost(req.stems_cost) }}){% endif %}
|
||||
|
|
@ -634,11 +388,11 @@
|
|||
{% endif %}
|
||||
{% if req.stems_link %}
|
||||
<div class="actions">
|
||||
<a href="{{ req.stems_link }}" target="_blank" rel="noopener noreferrer" class="button-link" style="display:inline-flex;align-items:center;gap:.4rem;padding:.5rem 1rem;background:#4b5563;border-radius:.375rem;color:#fff;text-decoration:none;font-size:.875rem">⬇ Download Stems</a>
|
||||
<a href="{{ req.stems_link }}" target="_blank" rel="noopener noreferrer" class="button-link">⬇ Download Stems</a>
|
||||
</div>
|
||||
{% elif req.stems_url %}
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('admin_download_stems', rid=req.id) }}" class="button-link" style="display:inline-flex;align-items:center;gap:.4rem;padding:.5rem 1rem;background:#4b5563;border-radius:.375rem;color:#fff;text-decoration:none;font-size:.875rem">⬇ Download Stems (zip)</a>
|
||||
<a href="{{ url_for('admin_download_stems', rid=req.id) }}" class="button-link">⬇ Download Stems (zip)</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -649,35 +403,33 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Notify Customer -->
|
||||
<div class="section">
|
||||
<div class="section" id="notify">
|
||||
<h2>3. Notify Customer</h2>
|
||||
<p>
|
||||
<p class="mt-0">
|
||||
Preview email:
|
||||
{% if req.preview_sent_at %}
|
||||
<span class="status-badge sent">✅ Sent</span> {{ req.preview_sent_at|regina_dt }}
|
||||
<span class="status-badge sent">Sent</span> {{ req.preview_sent_at|regina_dt }}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not sent yet</span>
|
||||
<span class="status-badge missing">Not sent yet</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>Both songs must be uploaded first.</p>
|
||||
<p class="copy-hint">Both songs must be uploaded first.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="notify_customer">
|
||||
<button type="submit" {% if not (req.song_a_path and req.song_b_path) %}disabled{% endif %}>Send Preview Link</button>
|
||||
</form>
|
||||
|
||||
{% if req.song_a_path and req.song_b_path %}
|
||||
<p style="margin-top:.8rem">
|
||||
<p class="mt-4 mb-0">
|
||||
Operator preview link:
|
||||
<a href="{{ url_for('play', token=req.player_token) }}" target="_blank" rel="noopener noreferrer">Open customer player in new tab →</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Payment & Delivery -->
|
||||
<div class="section">
|
||||
<div class="section" id="delivery">
|
||||
<h2>4. Payment & Delivery</h2>
|
||||
<p>
|
||||
<p class="mt-0">
|
||||
Customer approved:
|
||||
{% if req.customer_approved == 'none' %}
|
||||
<span class="approved-box">Nothing yet</span>
|
||||
|
|
@ -688,34 +440,33 @@
|
|||
<p>
|
||||
Delivery email:
|
||||
{% if req.delivery_sent_at %}
|
||||
<span class="status-badge sent">✅ Sent</span> {{ req.delivery_sent_at|regina_dt }}
|
||||
<span class="status-badge sent">Sent</span> {{ req.delivery_sent_at|regina_dt }}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not sent yet</span>
|
||||
<span class="status-badge missing">Not sent yet</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if req.square_payment_ref %}
|
||||
<p><strong>Square Payment Reference:</strong> {{ req.square_payment_ref }}</p>
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
<!-- Shared payment reference field -->
|
||||
<label for="square_payment_ref">Square Payment Reference</label>
|
||||
<input type="text" id="square_payment_ref" name="square_payment_ref" value="{{ req.square_payment_ref or '' }}" placeholder="e.g. sq0idp-... or receipt number">
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" name="action" value="update_payment_ref" class="secondary">Update Payment Reference</button>
|
||||
<button type="submit" name="action" value="update_payment_ref" class="button-secondary">Update Payment Reference</button>
|
||||
</div>
|
||||
|
||||
<hr style="border-color:#374151;margin:1rem 0">
|
||||
<hr class="divider">
|
||||
|
||||
<p style="color:#93c5fd;font-weight:600">Delivery options:</p>
|
||||
<label for="deliver_wav" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
|
||||
<input type="checkbox" id="deliver_wav" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %} style="width:auto;margin:0">
|
||||
<p style="color:var(--text-link);font-weight:600">Delivery options:</p>
|
||||
<label for="deliver_wav" class="checkbox-label">
|
||||
<input type="checkbox" id="deliver_wav" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %}>
|
||||
Include WAV files in delivery email
|
||||
</label>
|
||||
|
||||
<hr style="border-color:#374151;margin:1rem 0">
|
||||
<hr class="divider">
|
||||
|
||||
<p style="color:#93c5fd;font-weight:600">Select files to deliver:</p>
|
||||
<p style="color:var(--text-link);font-weight:600">Select files to deliver:</p>
|
||||
{% set all_files = [] %}
|
||||
{% if req.song_a_path and file_exists(req.song_a_path) %}
|
||||
{% set _ = all_files.append(req.song_a_path) %}
|
||||
|
|
@ -735,9 +486,7 @@
|
|||
{% set files_to_show = all_files + extra_files %}
|
||||
{% for fpath in files_to_show %}
|
||||
<div class="file-select">
|
||||
<input type="checkbox" id="file_{{ loop.index }}" name="deliver_file" value="{{ fpath }}"
|
||||
{% if fpath in all_files %}checked{% endif %}
|
||||
{% if req.customer_approved == 'none' %}disabled{% endif %}>
|
||||
<input type="checkbox" id="file_{{ loop.index }}" name="deliver_file" value="{{ fpath }}" {% if fpath in all_files %}checked{% endif %} {% if req.customer_approved == 'none' %}disabled{% endif %}>
|
||||
<label for="file_{{ loop.index }}"{% if req.customer_approved == 'none' %} style="opacity:.6"{% endif %}>{{ basename(fpath) }}</label>
|
||||
{% if not (fpath == req.song_a_path or fpath == req.song_b_path) %}
|
||||
<div class="old-rev">archived / revision file</div>
|
||||
|
|
@ -748,10 +497,10 @@
|
|||
{% endfor %}
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" name="action" value="mark_paid_deliver" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
||||
<button type="submit" name="action" value="mark_paid_deliver" class="button-success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
||||
</div>
|
||||
{% if req.customer_approved == 'none' %}
|
||||
<p style="margin-top:.5rem;color:#f87171;font-weight:600">⚠️ Customer must approve a version before you can mark paid or deliver.</p>
|
||||
<p class="text-danger" style="margin-top:var(--space-2);font-weight:600">⚠️ Customer must approve a version before you can mark paid or deliver.</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -517,20 +349,5 @@
|
|||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -7,193 +7,20 @@
|
|||
{% if refresh_seconds and refresh_seconds > 0 %}
|
||||
<meta http-equiv="refresh" content="{{ refresh_seconds }}">
|
||||
{% endif %}
|
||||
<style>
|
||||
*{box-sizing:border-box;}
|
||||
html,body{
|
||||
margin:0;
|
||||
padding:0;
|
||||
height:100%;
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#0f172a 0%,#1e3a8a 50%,#0f172a 100%);
|
||||
color:#f3f4f6;
|
||||
overflow:hidden;
|
||||
}
|
||||
.kiosk{
|
||||
height:100%;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
padding:2vh 3vw;
|
||||
text-align:center;
|
||||
}
|
||||
.banner{
|
||||
width:100%;
|
||||
max-height:22vh;
|
||||
object-fit:contain;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
}
|
||||
.status-pill{
|
||||
display:inline-block;
|
||||
margin-top:1.5vh;
|
||||
padding:.6rem 1.4rem;
|
||||
border-radius:9999px;
|
||||
font-weight:700;
|
||||
font-size:1.2rem;
|
||||
text-transform:uppercase;
|
||||
letter-spacing:.05em;
|
||||
}
|
||||
.status-open{background:#10b981;color:#000;}
|
||||
.status-closed{background:#ef4444;color:#000;}
|
||||
.stage{
|
||||
flex:1;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:100%;
|
||||
max-width:900px;
|
||||
margin:2vh 0;
|
||||
}
|
||||
.slide{
|
||||
width:100%;
|
||||
animation:fadeIn .8s ease;
|
||||
}
|
||||
@keyframes fadeIn{
|
||||
from{opacity:0;transform:translateY(20px);}
|
||||
to{opacity:1;transform:translateY(0);}
|
||||
}
|
||||
.slide.hidden{display:none;}
|
||||
h1{
|
||||
margin:0 0 1.5vh 0;
|
||||
font-size:clamp(2rem,5vw,3.5rem);
|
||||
color:#60a5fa;
|
||||
}
|
||||
.qr-code{
|
||||
max-height:44vh;
|
||||
max-width:44vh;
|
||||
width:auto;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
background:#fff;
|
||||
padding:.6rem;
|
||||
}
|
||||
.qr-caption{
|
||||
margin-top:1.5vh;
|
||||
font-size:clamp(1.1rem,2.5vw,1.6rem);
|
||||
color:#93c5fd;
|
||||
}
|
||||
.price-card{
|
||||
background:rgba(31,41,55,.85);
|
||||
border:1px solid rgba(96,165,250,.25);
|
||||
border-radius:1.2rem;
|
||||
padding:3vh 4vw;
|
||||
box-shadow:0 15px 50px rgba(0,0,0,.35);
|
||||
width:100%;
|
||||
}
|
||||
.price-card h2{
|
||||
margin:0 0 2vh 0;
|
||||
font-size:clamp(1.6rem,4vw,2.6rem);
|
||||
color:#fbbf24;
|
||||
}
|
||||
.price-list{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
text-align:left;
|
||||
}
|
||||
.price-list li{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
border-bottom:1px solid rgba(147,197,253,.2);
|
||||
padding:1.2vh 0;
|
||||
font-size:clamp(1.2rem,3vw,1.8rem);
|
||||
}
|
||||
.price-list li:last-child{border-bottom:none;}
|
||||
.price-list .label{color:#e5e7eb;}
|
||||
.price-list .amount{font-weight:700;color:#fbbf24;}
|
||||
.price-empty{
|
||||
font-size:1.3rem;
|
||||
color:#9ca3af;
|
||||
}
|
||||
.queue-card{
|
||||
background:rgba(31,41,55,.85);
|
||||
border:1px solid rgba(96,165,250,.25);
|
||||
border-radius:1.2rem;
|
||||
padding:3vh 4vw;
|
||||
box-shadow:0 15px 50px rgba(0,0,0,.35);
|
||||
width:100%;
|
||||
max-height:62vh;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
.queue-card h2{
|
||||
margin:0 0 2vh 0;
|
||||
font-size:clamp(1.6rem,4vw,2.6rem);
|
||||
color:#fbbf24;
|
||||
}
|
||||
.queue-list{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
text-align:left;
|
||||
overflow-y:auto;
|
||||
flex:1;
|
||||
}
|
||||
.queue-list li{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
border-bottom:1px solid rgba(147,197,253,.2);
|
||||
padding:1.4vh 0;
|
||||
font-size:clamp(1.2rem,2.8vw,1.7rem);
|
||||
}
|
||||
.queue-list li:last-child{border-bottom:none;}
|
||||
.queue-list .customer{color:#e5e7eb;font-weight:600;}
|
||||
.queue-list .status{font-weight:700;color:#60a5fa;text-align:right;}
|
||||
.queue-empty{
|
||||
font-size:1.4rem;
|
||||
color:#9ca3af;
|
||||
text-align:center;
|
||||
margin:auto 0;
|
||||
}
|
||||
.queue-note{
|
||||
font-size:1rem;
|
||||
color:#9ca3af;
|
||||
margin-top:1.5vh;
|
||||
text-align:center;
|
||||
}
|
||||
.footer{
|
||||
font-size:1rem;
|
||||
color:#9ca3af;
|
||||
}
|
||||
.dots{
|
||||
display:flex;
|
||||
gap:.6rem;
|
||||
justify-content:center;
|
||||
margin-top:1vh;
|
||||
}
|
||||
.dot{
|
||||
width:12px;
|
||||
height:12px;
|
||||
border-radius:50%;
|
||||
background:#374151;
|
||||
transition:background .3s;
|
||||
}
|
||||
.dot.active{background:#60a5fa;}
|
||||
</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>
|
||||
<body class="booth-page kiosk-page">
|
||||
<div class="kiosk">
|
||||
<div class="header">
|
||||
{% if booth_open %}
|
||||
<img src="{{ url_for('static', filename='Trollgorithm_booth.jpg') }}" alt="Trollgorithm Theme Song Booth" class="banner">
|
||||
<div class="status-pill status-open">Booth is Open ✅</div>
|
||||
<img src="{{ url_for('static', filename='Trollgorithm_booth.jpg') }}" alt="Trollgorithm Theme Song Booth" class="kiosk-banner">
|
||||
<div class="status-pill open">Booth is Open</div>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='Booth_closed.png') }}" alt="Booth Closed" class="banner">
|
||||
<div class="status-pill status-closed">Booth is Closed ❌</div>
|
||||
<img src="{{ url_for('static', filename='Booth_closed.png') }}" alt="Booth Closed" class="kiosk-banner">
|
||||
<div class="status-pill closed">Booth is Closed</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,144 +4,57 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Your Theme Song</title>
|
||||
<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') }}">
|
||||
<style>
|
||||
/*
|
||||
Private customer player page.
|
||||
Shows two audio players for Version A and Version B, plus approval
|
||||
buttons or a revision note form depending on request status.
|
||||
After the customer makes a choice, the controls are hidden and a
|
||||
confirmation/waiting message is shown. The number of remaining
|
||||
customer revisions is displayed when revisions are enabled.
|
||||
*/
|
||||
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:640px;
|
||||
margin:0 auto;
|
||||
background:#1f2937;
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
}
|
||||
h1{color:#60a5fa;}
|
||||
.player{
|
||||
background:#111827;
|
||||
padding:1rem;
|
||||
border-radius:.5rem;
|
||||
margin:1rem 0;
|
||||
}
|
||||
audio{width:100%;margin-top:.5rem;}
|
||||
.actions{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:1rem;}
|
||||
button{
|
||||
flex:1;
|
||||
min-width:120px;
|
||||
padding:.8rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:#3b82f6;
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
}
|
||||
button:disabled{background:#374151;color:#9ca3af;cursor:not-allowed;}
|
||||
button.selected{background:#10b981;}
|
||||
button.both{background:#8b5cf6;}
|
||||
button.revision{background:#f59e0b;color:#000;}
|
||||
form{margin-top:1rem;}
|
||||
textarea{
|
||||
width:100%;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
box-sizing:border-box;
|
||||
min-height:80px;
|
||||
}
|
||||
.status{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-top:1rem;
|
||||
}
|
||||
.status.waiting{background:#3f3f46;}
|
||||
.locked{
|
||||
margin-top:1rem;
|
||||
padding:1rem;
|
||||
background:#111827;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
}
|
||||
.locked h2{margin-top:0;color:#fbbf24;}
|
||||
.locked p{margin:.3rem 0;}
|
||||
.lyrics{
|
||||
background:#111827;
|
||||
border:1px solid #374151;
|
||||
border-radius:.5rem;
|
||||
padding:1rem 1.25rem;
|
||||
margin:1rem 0;
|
||||
line-height:1.3;
|
||||
font-size:1rem;
|
||||
}
|
||||
.lyrics h2{
|
||||
margin-top:0;
|
||||
margin-bottom:.5rem;
|
||||
color:#fbbf24;
|
||||
font-size:1.2rem;
|
||||
}
|
||||
.lyrics .section-label{
|
||||
color:#60a5fa;
|
||||
font-weight:700;
|
||||
display:block;
|
||||
margin-top:.5rem;
|
||||
margin-bottom:.1rem;
|
||||
}
|
||||
.lyrics .lyric-line{
|
||||
display:block;
|
||||
line-height:1.3;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
.verse + .verse { margin-top: var(--space-5); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body class="booth-page">
|
||||
<div class="container">
|
||||
<h1>🎧 Your Custom Theme Song</h1>
|
||||
<h1>Your Custom Theme Song</h1>
|
||||
<p>Hi {{ req.name }}! Listen to both versions and pick the one you want.</p>
|
||||
|
||||
{% if req.suno_lyrics %}
|
||||
<div class="lyrics">
|
||||
<h2>🎵 Song Lyrics</h2>
|
||||
<h2>Song Lyrics</h2>
|
||||
{% set current_verse = namespace(lines=[]) %}
|
||||
{% for line in req.suno_lyrics.splitlines() %}
|
||||
{% set line = line.strip() %}
|
||||
{% if line and not (line.startswith('[') and line.endswith(']')) %}
|
||||
<span class="lyric-line">{{ line }}</span>
|
||||
{% elif not line %}
|
||||
<br>
|
||||
{% set _ = current_verse.lines.append(line) %}
|
||||
{% elif not line and current_verse.lines %}
|
||||
<div class="verse">
|
||||
{% for l in current_verse.lines %}
|
||||
<span class="lyric-line">{{ l }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% set current_verse.lines = [] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if current_verse.lines %}
|
||||
<div class="verse">
|
||||
{% for l in current_verse.lines %}
|
||||
<span class="lyric-line">{{ l }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Album cover -->
|
||||
{% if req.album_cover_url and deliver_album_cover %}
|
||||
<div style="text-align:center;margin:1rem 0">
|
||||
<img src="{{ req.album_cover_url }}" alt="Album cover" style="max-width:160px;border-radius:.5rem">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Version A player -->
|
||||
{% if req.album_cover_url and deliver_album_cover %}
|
||||
<div class="text-center mt-4 mb-4">
|
||||
<img src="{{ req.album_cover_url }}" alt="Album cover" style="max-width:160px;border-radius:var(--radius-md)">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="player">
|
||||
<h3>Version A</h3>
|
||||
<audio controls controlsList="nodownload" id="player-a"></audio>
|
||||
</div>
|
||||
|
||||
<!-- Version B player -->
|
||||
<div class="player">
|
||||
<h3>Version B</h3>
|
||||
<audio controls controlsList="nodownload" id="player-b"></audio>
|
||||
|
|
@ -149,24 +62,21 @@
|
|||
|
||||
<script>
|
||||
(function(){
|
||||
// Audio sources are assigned via JS so they do not appear in static HTML source.
|
||||
document.getElementById('player-a').src = "{{ url_for('stream_audio', token=req.player_token, version='a') }}";
|
||||
document.getElementById('player-b').src = "{{ url_for('stream_audio', token=req.player_token, version='b') }}";
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% if req.status == 'revisions_requested' %}
|
||||
<!-- Customer asked for changes; lock the page and tell them to wait -->
|
||||
<div class="locked waiting">
|
||||
<h2>📝 Revision Requested</h2>
|
||||
<h2>Revision Requested</h2>
|
||||
<p>You asked for changes. We will generate a new version and update this page.</p>
|
||||
<p><strong>Your note:</strong> {{ req.revision_note }}</p>
|
||||
</div>
|
||||
|
||||
{% elif req.status in ['awaiting_payment','paid','delivered'] %}
|
||||
<!-- Customer already picked a version; show their choice and payment instruction -->
|
||||
<div class="locked">
|
||||
<h2>✅ Choice Received</h2>
|
||||
<h2>Choice Received</h2>
|
||||
<p>You selected: <strong>{% if req.customer_approved == 'both' %}Both Versions{% else %}Version {{ (req.customer_approved or 'none').upper() }}{% endif %}</strong></p>
|
||||
{% if req.status == 'awaiting_payment' %}
|
||||
<p>Please return to the booth to finalize payment and collect your files.</p>
|
||||
|
|
@ -175,37 +85,35 @@
|
|||
{% elif req.status == 'delivered' %}
|
||||
<p>Delivered! Check your email for the MP3 attachment(s).</p>
|
||||
{% if req.stems_link %}
|
||||
<p style="margin-top:.8rem">
|
||||
<a href="{{ req.stems_link }}" target="_blank" rel="noopener noreferrer" style="display:inline-block;padding:.7rem 1rem;background:#10b981;color:#000;border-radius:.5rem;text-decoration:none;font-weight:700;">Download stems / extras →</a>
|
||||
<p class="mt-4">
|
||||
<a href="{{ req.stems_link }}" target="_blank" rel="noopener noreferrer" class="button">Download stems / extras</a>
|
||||
</p>
|
||||
<p style="font-size:.9rem;color:#9ca3af;">This link expires 3 months after delivery. Download soon.</p>
|
||||
<p class="text-muted" style="font-size:0.9rem">This link expires 3 months after delivery. Download soon.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Approval form: customer picks Version A, B, or both -->
|
||||
<form method="POST" action="{{ url_for('approve', token=req.player_token) }}">
|
||||
<input type="hidden" name="choice" id="choice">
|
||||
<div class="actions">
|
||||
<button type="submit" onclick="document.getElementById('choice').value='a'">I want Version A</button>
|
||||
<button type="submit" onclick="document.getElementById('choice').value='b'">I want Version B</button>
|
||||
<button type="submit" class="both" onclick="document.getElementById('choice').value='both'">I want both</button>
|
||||
<button type="submit" class="button-secondary" onclick="document.getElementById('choice').value='a'">I want Version A</button>
|
||||
<button type="submit" class="button-secondary" onclick="document.getElementById('choice').value='b'">I want Version B</button>
|
||||
<button type="submit" onclick="document.getElementById('choice').value='both'">I want both</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if revisions_left > 0 %}
|
||||
<!-- Revision form: customer asks for changes -->
|
||||
<form method="POST" action="{{ url_for('revise', token=req.player_token) }}">
|
||||
<p style="color:#93c5fd;font-weight:600">Revisions remaining: {{ revisions_left }}</p>
|
||||
<p class="text-accent" style="font-weight:600">Revisions remaining: {{ revisions_left }}</p>
|
||||
<label for="revision_note">Or ask for changes:</label>
|
||||
<textarea id="revision_note" name="revision_note" placeholder="e.g. make the chorus louder, swap a lyric..."></textarea>
|
||||
<div class="actions">
|
||||
<button type="submit" class="revision">Request Changes</button>
|
||||
<button type="submit" class="button-secondary">Request Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p style="margin-top:1rem;color:#f87171;font-weight:600">No revisions remaining. Please speak to the booth operator if you need further changes.</p>
|
||||
<p class="text-danger" style="margin-top:var(--space-4);font-weight:600">No revisions remaining. Please speak to the booth operator if you need further changes.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,126 +4,23 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request Your Theme Song</title>
|
||||
<style>
|
||||
/*
|
||||
Customer-facing request page.
|
||||
Designed for convention booth tablets/phones. Shows the banner image and
|
||||
a styled card form for name, email, hobbies, facts, style, vocal gender,
|
||||
and extra requests. Hidden when the booth is marked closed.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#111827 0%,#1e3a8a 50%,#111827 100%);
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
min-height:100vh;
|
||||
}
|
||||
.container{max-width:680px;margin:0 auto;}
|
||||
.banner{
|
||||
width:100%;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
.card{
|
||||
background:rgba(31,41,55,.9);
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 25px rgba(0,0,0,.3);
|
||||
border:1px solid rgba(96,165,250,.2);
|
||||
}
|
||||
h1{
|
||||
margin-top:0;
|
||||
color:#60a5fa;
|
||||
text-align:center;
|
||||
font-size:1.7rem;
|
||||
}
|
||||
.subtitle{
|
||||
text-align:center;
|
||||
color:#9ca3af;
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
label{
|
||||
display:block;
|
||||
margin-top:1rem;
|
||||
font-weight:600;
|
||||
color:#93c5fd;
|
||||
}
|
||||
input,textarea{
|
||||
width:100%;
|
||||
padding:.7rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
font:inherit;
|
||||
margin-top:.25rem;
|
||||
}
|
||||
select{
|
||||
width:100%;
|
||||
padding:.7rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
font:inherit;
|
||||
margin-top:.25rem;
|
||||
}
|
||||
input:focus,textarea:focus,select:focus{
|
||||
outline:none;
|
||||
border-color:#60a5fa;
|
||||
box-shadow:0 0 0 3px rgba(96,165,250,.2);
|
||||
}
|
||||
textarea{min-height:90px;resize:vertical;}
|
||||
button{
|
||||
margin-top:1.5rem;
|
||||
width:100%;
|
||||
padding:1rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:linear-gradient(90deg,#3b82f6,#8b5cf6);
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
font-size:1.1rem;
|
||||
cursor:pointer;
|
||||
box-shadow:0 4px 15px rgba(59,130,246,.4);
|
||||
transition:transform .1s,box-shadow .1s;
|
||||
}
|
||||
button:hover{
|
||||
transform:translateY(-2px);
|
||||
box-shadow:0 6px 20px rgba(139,92,246,.5);
|
||||
}
|
||||
.note{
|
||||
margin-top:1rem;
|
||||
font-size:.9rem;
|
||||
color:#9ca3af;
|
||||
text-align:center;
|
||||
}
|
||||
.sparkle{
|
||||
font-size:1.3rem;
|
||||
vertical-align:middle;
|
||||
}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
</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>
|
||||
<body class="booth-page">
|
||||
<div class="container">
|
||||
<!-- Banner graphic for the booth. Served from /static. -->
|
||||
<img src="{{ url_for('static', filename='Trollgorithm_booth.jpg') }}" alt="Trollgorithm Theme Song Booth" class="banner">
|
||||
|
||||
<div class="card">
|
||||
<h1><span class="sparkle">🎵</span> Get Your Custom Theme Song <span class="sparkle">🎶</span></h1>
|
||||
<h1 class="text-center">Get Your Custom Theme Song</h1>
|
||||
<p class="subtitle">Tell us about yourself and we'll craft a one-of-a-kind song just for you.</p>
|
||||
<p class="subtitle"><a href="{{ url_for('faq') }}" target="_blank" rel="noopener noreferrer">Questions? Read our FAQ →</a> · <a href="{{ url_for('status_lookup') }}" target="_blank" rel="noopener noreferrer">Already ordered? Check status →</a></p>
|
||||
<p class="subtitle">
|
||||
<a href="{{ url_for('faq') }}" target="_blank" rel="noopener noreferrer">Questions? Read our FAQ</a>
|
||||
·
|
||||
<a href="{{ url_for('status_lookup') }}" target="_blank" rel="noopener noreferrer">Already ordered? Check status</a>
|
||||
</p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
|
|
@ -140,7 +37,7 @@
|
|||
<label for="name">Name / persona you want in the song</label>
|
||||
<input type="text" id="name" name="name" value="{{ form.name if form else '' }}" required>
|
||||
|
||||
<label for="pronouns">Pronouns <span style="color:#f87171">*</span></label>
|
||||
<label for="pronouns">Pronouns <span class="required">*</span></label>
|
||||
<select id="pronouns" name="pronouns" required>
|
||||
<option value="" {% if not form or not form.pronouns %}selected{% endif %}>— Select pronouns —</option>
|
||||
<option value="He/Him/His" {% if form and form.pronouns == 'He/Him/His' %}selected{% endif %}>He/Him/His</option>
|
||||
|
|
@ -154,7 +51,7 @@
|
|||
<label for="notable_facts">Notable things about you</label>
|
||||
<textarea id="notable_facts" name="notable_facts" maxlength="2000" placeholder="Anything fun, weird, or heroic we should mention">{{ form.notable_facts if form else '' }}</textarea>
|
||||
|
||||
<label for="decade">Decade / era <span style="color:#f87171">*</span></label>
|
||||
<label for="decade">Decade / era <span class="required">*</span></label>
|
||||
<select id="decade" name="decade" required>
|
||||
<option value="" {% if not form or not form.decade %}selected{% endif %}>— Select decade —</option>
|
||||
{% for d in decades %}
|
||||
|
|
@ -162,7 +59,7 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="basic_style">Basic style <span style="color:#f87171">*</span></label>
|
||||
<label for="basic_style">Basic style <span class="required">*</span></label>
|
||||
<select id="basic_style" name="basic_style" required>
|
||||
<option value="" {% if not form or not form.basic_style %}selected{% endif %}>— Select style —</option>
|
||||
{% for g in genres %}
|
||||
|
|
@ -178,10 +75,9 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Selected style: <span id="style-preview">{% if form %}{{ form.decade or '' }}{% if form.decade and form.basic_style %}, {% endif %}{{ form.basic_style or '' }}{% if form.basic_style and form.additional_style %}, {% endif %}{{ form.additional_style or '' }}{% else %}—{% endif %}</span></p>
|
||||
<p class="copy-hint">Selected style: <span id="style-preview">{% if form %}{{ form.decade or '' }}{% if form.decade and form.basic_style %}, {% endif %}{{ form.basic_style or '' }}{% if form.basic_style and form.additional_style %}, {% endif %}{{ form.additional_style or '' }}{% else %}—{% endif %}</span></p>
|
||||
|
||||
<script>
|
||||
// Live preview of the combined style string as the customer changes dropdowns.
|
||||
(function(){
|
||||
const decade = document.getElementById('decade');
|
||||
const basic = document.getElementById('basic_style');
|
||||
|
|
@ -210,14 +106,14 @@
|
|||
<label for="extra_requests">Anything else you want in the song? (Trollgorithm is very literal, so be careful!)</label>
|
||||
<textarea id="extra_requests" name="extra_requests" maxlength="2000" placeholder="Specific Lyrics, clean/explicit...">{{ form.extra_requests if form else '' }}</textarea>
|
||||
|
||||
<label for="stems_interest" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
|
||||
<input type="checkbox" id="stems_interest" name="stems_interest" value="1" {% if form and form.stems_interest %}checked{% endif %} style="width:auto;margin:0">
|
||||
<label for="stems_interest" class="checkbox-label">
|
||||
<input type="checkbox" id="stems_interest" name="stems_interest" value="1" {% if form and form.stems_interest %}checked{% endif %}>
|
||||
I'm interested in STEMS / multitrack files (if you know, you know)
|
||||
</label>
|
||||
|
||||
<button type="submit">Submit Request</button>
|
||||
<button type="submit" class="button-block">Submit Request</button>
|
||||
</form>
|
||||
<p class="note">Your info is only used to create and deliver your song.</p>
|
||||
<p class="subtitle mb-0">Your info is only used to create and deliver your song.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue