Add Live Queue slide to kiosk rotation
- Added a third kiosk slide showing the active customer queue.
- Customer names are derived from the local-part of their email address.
- Statuses are mapped to friendly labels:
pending -> Received
prompt_ready / songs_uploaded -> Trollgorithm Recording in Studio
revisions_requested -> Waiting for Customer Response
awaiting_payment -> Payment Due
- Completed/delivered statuses are not shown.
- If the queue overflows, a note points customers to the status page.
- Added queue-only mode (kiosk_cycle_seconds = 1) and updated the
cycle logic to rotate QR -> pricing -> queue.
- Updated /admin/settings help text for the new mode.
Bumps version to 0.5.3.
This commit is contained in:
parent
96b6470c25
commit
52558010e8
5 changed files with 129 additions and 19 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Theme Song Booth
|
# Theme Song Booth
|
||||||
|
|
||||||
**Version:** `v0.5.2`
|
**Version:** `v0.5.3`
|
||||||
|
|
||||||
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 Suno prompts, upload MP3 previews, collect payment, and deliver final songs by email.
|
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 Suno prompts, upload MP3 previews, collect payment, and deliver final songs by email.
|
||||||
|
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.5.2
|
0.5.3
|
||||||
|
|
|
||||||
30
app.py
30
app.py
|
|
@ -350,7 +350,10 @@ def get_kiosk_cycle_seconds():
|
||||||
|
|
||||||
|
|
||||||
def get_kiosk_mode():
|
def get_kiosk_mode():
|
||||||
"""Return 'cycle', 'qr', or 'prices' based on kiosk_cycle_seconds setting."""
|
"""
|
||||||
|
Return 'qr', 'prices', 'queue', or 'cycle' based on kiosk_cycle_seconds setting.
|
||||||
|
-1 = QR only, 0 = prices only, 1 = queue only, 5+ = cycle through all three.
|
||||||
|
"""
|
||||||
cfg = load_booth_settings()
|
cfg = load_booth_settings()
|
||||||
try:
|
try:
|
||||||
val = int(cfg.get('kiosk_cycle_seconds', 10))
|
val = int(cfg.get('kiosk_cycle_seconds', 10))
|
||||||
|
|
@ -360,6 +363,8 @@ def get_kiosk_mode():
|
||||||
return 'qr'
|
return 'qr'
|
||||||
if val == 0:
|
if val == 0:
|
||||||
return 'prices'
|
return 'prices'
|
||||||
|
if val == 1:
|
||||||
|
return 'queue'
|
||||||
return 'cycle'
|
return 'cycle'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1017,12 +1022,35 @@ def kiosk():
|
||||||
mode = get_kiosk_mode()
|
mode = get_kiosk_mode()
|
||||||
booth_open = get_booth_open()
|
booth_open = get_booth_open()
|
||||||
|
|
||||||
|
# Build the public queue: only active statuses, mapped to friendly names,
|
||||||
|
# using the local-part of the email as the customer name.
|
||||||
|
KIOSK_STATUS_MAP = {
|
||||||
|
'pending': 'Received',
|
||||||
|
'prompt_ready': 'Trollgorithm Recording in Studio',
|
||||||
|
'songs_uploaded': 'Trollgorithm Recording in Studio',
|
||||||
|
'revisions_requested': 'Waiting for Customer Response',
|
||||||
|
'awaiting_payment': 'Payment Due',
|
||||||
|
}
|
||||||
|
active_statuses = set(KIOSK_STATUS_MAP.keys())
|
||||||
|
raw_queue = list_requests()
|
||||||
|
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
|
||||||
|
queue.append({
|
||||||
|
'name': name or 'Guest',
|
||||||
|
'status': KIOSK_STATUS_MAP[r['status']],
|
||||||
|
'raw_status': r['status'],
|
||||||
|
})
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'kiosk.html',
|
'kiosk.html',
|
||||||
booth_open=booth_open,
|
booth_open=booth_open,
|
||||||
price_items=price_items,
|
price_items=price_items,
|
||||||
cycle_seconds=cycle_seconds,
|
cycle_seconds=cycle_seconds,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
|
queue=queue,
|
||||||
refresh_seconds=30
|
refresh_seconds=30
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -272,12 +272,12 @@
|
||||||
<!-- Kiosk cycle -->
|
<!-- Kiosk cycle -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Kiosk Display Mode</h2>
|
<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, or 5+ seconds to cycle between them.</p>
|
<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>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="action" value="save_kiosk_cycle">
|
<input type="hidden" name="action" value="save_kiosk_cycle">
|
||||||
<label for="kiosk_cycle_seconds">Kiosk cycle seconds</label>
|
<label for="kiosk_cycle_seconds">Kiosk cycle seconds</label>
|
||||||
<input type="number" id="kiosk_cycle_seconds" name="kiosk_cycle_seconds" value="{{ current_kiosk_cycle_seconds }}" min="-1">
|
<input type="number" id="kiosk_cycle_seconds" name="kiosk_cycle_seconds" value="{{ current_kiosk_cycle_seconds }}" min="-1">
|
||||||
<p class="copy-hint">Examples: -1 = QR only, 0 = pricing only, 10 = swap every 10 seconds.</p>
|
<p class="copy-hint">Examples: -1 = QR only, 0 = pricing only, 1 = queue only, 10 = rotate QR → pricing → queue every 10 seconds.</p>
|
||||||
<button type="submit">Save Kiosk Mode</button>
|
<button type="submit">Save Kiosk Mode</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,53 @@
|
||||||
font-size:1.3rem;
|
font-size:1.3rem;
|
||||||
color:#9ca3af;
|
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{
|
.footer{
|
||||||
font-size:1rem;
|
font-size:1rem;
|
||||||
color:#9ca3af;
|
color:#9ca3af;
|
||||||
|
|
@ -175,6 +222,25 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% elif mode == 'queue' %}
|
||||||
|
<div class="slide">
|
||||||
|
<div class="queue-card">
|
||||||
|
<h2>Live Queue</h2>
|
||||||
|
{% if queue %}
|
||||||
|
<ul class="queue-list">
|
||||||
|
{% for item in queue %}
|
||||||
|
<li>
|
||||||
|
<span class="customer">{{ item.name }}</span>
|
||||||
|
<span class="status">{{ item.status }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="queue-note">If your request isn't shown, visit the request page and use the status link to check your place.</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="queue-empty">No active requests right now. Be the first!</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="slide" id="slide-qr">
|
<div class="slide" id="slide-qr">
|
||||||
<h1>Scan to Order</h1>
|
<h1>Scan to Order</h1>
|
||||||
|
|
@ -198,6 +264,24 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="slide hidden" id="slide-queue">
|
||||||
|
<div class="queue-card">
|
||||||
|
<h2>Live Queue</h2>
|
||||||
|
{% if queue %}
|
||||||
|
<ul class="queue-list">
|
||||||
|
{% for item in queue %}
|
||||||
|
<li>
|
||||||
|
<span class="customer">{{ item.name }}</span>
|
||||||
|
<span class="status">{{ item.status }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="queue-note">If your request isn't shown, visit the request page and use the status link to check your place.</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="queue-empty">No active requests right now. Be the first!</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -206,30 +290,28 @@
|
||||||
<div class="dots">
|
<div class="dots">
|
||||||
<div class="dot active" id="dot-qr"></div>
|
<div class="dot active" id="dot-qr"></div>
|
||||||
<div class="dot" id="dot-prices"></div>
|
<div class="dot" id="dot-prices"></div>
|
||||||
|
<div class="dot" id="dot-queue"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
const qr = document.getElementById('slide-qr');
|
const qr = document.getElementById('slide-qr');
|
||||||
const prices = document.getElementById('slide-prices');
|
const prices = document.getElementById('slide-prices');
|
||||||
|
const queue = document.getElementById('slide-queue');
|
||||||
const dotQr = document.getElementById('dot-qr');
|
const dotQr = document.getElementById('dot-qr');
|
||||||
const dotPrices = document.getElementById('dot-prices');
|
const dotPrices = document.getElementById('dot-prices');
|
||||||
|
const dotQueue = document.getElementById('dot-queue');
|
||||||
const seconds = {{ cycle_seconds }};
|
const seconds = {{ cycle_seconds }};
|
||||||
if (seconds > 0 && qr && prices) {
|
const slides = [qr, prices, queue];
|
||||||
let showingQr = true;
|
const dots = [dotQr, dotPrices, dotQueue];
|
||||||
|
if (seconds > 0) {
|
||||||
|
let idx = 0;
|
||||||
setInterval(function(){
|
setInterval(function(){
|
||||||
showingQr = !showingQr;
|
slides[idx].classList.add('hidden');
|
||||||
if (showingQr) {
|
dots[idx].classList.remove('active');
|
||||||
qr.classList.remove('hidden');
|
idx = (idx + 1) % slides.length;
|
||||||
prices.classList.add('hidden');
|
slides[idx].classList.remove('hidden');
|
||||||
dotQr.classList.add('active');
|
dots[idx].classList.add('active');
|
||||||
dotPrices.classList.remove('active');
|
|
||||||
} else {
|
|
||||||
qr.classList.add('hidden');
|
|
||||||
prices.classList.remove('hidden');
|
|
||||||
dotQr.classList.remove('active');
|
|
||||||
dotPrices.classList.add('active');
|
|
||||||
}
|
|
||||||
}, seconds * 1000);
|
}, seconds * 1000);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Reference in a new issue