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,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 &amp; 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>
<div class="step-label">{{ mg_label }}</div>
</li>
{% set mg_key, mg_label = mg_steps[j] %}
<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) }}
{% 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>
{% endif %}
<p class="mt-2"><strong>LLM Cost:</strong> {{ format_musicgpt_cost(total_cost) }}
{% if req.stems_cost %}
<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 &amp; 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 &amp; Delivery -->
<div class="section">
<div class="section" id="delivery">
<h2>4. Payment &amp; 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 &amp; Deliver</button>
<button type="submit" name="action" value="mark_paid_deliver" class="button-success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid &amp; 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>