Treat all Now Serving cards equally and announce every ticket change.
Deploy Ladill Queue / deploy (push) Successful in 1m56s

Remove newest-card highlighting so every active service point looks the same, sort the board stably by destination, and have the display announce whenever a counter’s ticket changes (falling back when a server voice row is missing).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 18:20:45 +00:00
co-authored by Cursor
parent 9a11098674
commit 5356118fd8
5 changed files with 105 additions and 38 deletions
-20
View File
@@ -491,20 +491,9 @@ html:has(.qms-display) body {
box-shadow: 0 16px 32px -20px rgb(15 23 42 / 0.2);
}
.qms-display__ticket--newest {
border-color: rgb(129 140 248);
background: linear-gradient(145deg, rgb(238 242 255) 0%, #fff 46%);
box-shadow: 0 24px 48px -24px rgb(79 70 229 / 0.45);
}
.qms-display__ticket-accent {
height: 4px;
flex-shrink: 0;
background: rgb(203 213 225);
}
.qms-display__ticket--newest .qms-display__ticket-accent {
height: 6px;
background: linear-gradient(90deg, rgb(67 56 202), rgb(124 58 237), rgb(99 102 241));
}
@@ -539,11 +528,6 @@ html:has(.qms-display) body {
color: #fff;
}
.qms-display__ticket:not(.qms-display__ticket--newest) .qms-display__badge {
background: rgb(241 245 249);
color: rgb(71 85 105);
}
.qms-display__queue-name {
min-width: 0;
overflow: hidden;
@@ -573,10 +557,6 @@ html:has(.qms-display) body {
color: rgb(30 41 59);
}
.qms-display__ticket--newest .qms-display__ticket-number {
color: rgb(67 56 202);
}
.qms-display__destination {
flex-shrink: 0;
min-width: 0;
+81 -4
View File
@@ -202,6 +202,9 @@ export function registerKioskFlow(Alpine) {
Alpine.data('displayBoard', (config = {}) => ({
payload: config.initial ?? null,
announcedIds: {},
servingSignatures: {},
servingPrimed: false,
pendingLocalAnnouncements: [],
isAnnouncing: false,
speechReady: false,
voices: [],
@@ -229,6 +232,10 @@ export function registerKioskFlow(Alpine) {
}
this.payload = await res.json();
this.pollError = null;
this.trackServingChanges(
this.payload?.now_serving ?? [],
this.payload?.announcements ?? [],
);
this.maybeAnnounce(this.payload?.announcements ?? []);
} catch (e) {
this.pollError = e.message || 'Could not refresh display data';
@@ -236,6 +243,64 @@ export function registerKioskFlow(Alpine) {
}
},
servingPointKey(item) {
return item?.point_key
|| item?.destination
|| item?.counter
|| item?.queue_name
|| item?.ticket_number
|| 'unknown';
},
trackServingChanges(nowServing, serverAnnouncements = []) {
const nextSignatures = {};
const changed = [];
for (const item of nowServing) {
if (! item?.ticket_number) {
continue;
}
const key = this.servingPointKey(item);
const signature = `${item.ticket_number}|${item.called_at || ''}`;
nextSignatures[key] = signature;
if (this.servingPrimed && this.servingSignatures[key] !== signature) {
changed.push(item);
}
}
this.servingSignatures = nextSignatures;
if (! this.servingPrimed) {
this.servingPrimed = true;
return;
}
for (const item of changed) {
const ticket = String(item.ticket_number);
const coveredByServer = (serverAnnouncements || []).some(
(announcement) => announcement?.message && String(announcement.message).includes(ticket),
);
if (coveredByServer) {
continue;
}
const message = item.announcement_text
|| `Ticket ${item.ticket_number}, please proceed to ${item.destination || item.counter || 'the service point'}.`;
const localId = `local:${this.servingPointKey(item)}:${item.ticket_number}:${item.called_at || Date.now()}`;
if (this.announcedIds[localId]) {
continue;
}
this.pendingLocalAnnouncements.push({
id: localId,
message,
locale: 'en',
voice: 'female',
volume: 80,
repeat_count: 1,
local: true,
});
}
},
primeSpeech() {
if (! window.speechSynthesis) {
return;
@@ -277,17 +342,22 @@ export function registerKioskFlow(Alpine) {
},
maybeAnnounce(announcements) {
if (! window.speechSynthesis || ! this.speechReady || this.isAnnouncing || ! announcements.length) {
if (! window.speechSynthesis || ! this.speechReady || this.isAnnouncing) {
return;
}
const next = announcements.find((item) => item?.id && item?.message && ! this.announcedIds[item.id]);
const queue = [
...(Array.isArray(announcements) ? announcements : []),
...this.pendingLocalAnnouncements,
];
const next = queue.find((item) => item?.id && item?.message && ! this.announcedIds[item.id]);
if (! next) {
return;
}
this.isAnnouncing = true;
this.announcedIds[next.id] = true;
this.pendingLocalAnnouncements = this.pendingLocalAnnouncements.filter((item) => item.id !== next.id);
this.speakAnnouncement(next);
},
@@ -307,8 +377,13 @@ export function registerKioskFlow(Alpine) {
const finish = () => {
release();
this.ackPlayed(item.id);
this.maybeAnnounce(this.payload?.announcements ?? []);
if (! item.local) {
this.ackPlayed(item.id);
}
this.maybeAnnounce([
...(this.payload?.announcements ?? []),
...this.pendingLocalAnnouncements,
]);
};
const fail = (reason) => {
@@ -400,6 +475,8 @@ export function registerKioskFlow(Alpine) {
// ignore storage failures
}
// Seed signatures from SSR payload without announcing historical tickets.
this.trackServingChanges(this.payload?.now_serving ?? []);
this.poll();
setInterval(() => this.poll(), this.pollMs);
},
+2 -5
View File
@@ -82,11 +82,8 @@
x-show="(payload?.now_serving ?? []).length"
:data-ticket-count="Math.min((payload?.now_serving ?? []).length, 12)"
>
<template x-for="(item, index) in (payload?.now_serving ?? [])" :key="item.ticket_number + (item.counter ?? '')">
<article
class="qms-display__ticket rounded-2xl lg:rounded-3xl"
:class="{ 'qms-display__ticket--newest': index === 0 }"
>
<template x-for="item in (payload?.now_serving ?? [])" :key="(item.destination || item.counter || item.queue_name || '') + ':' + item.ticket_number">
<article class="qms-display__ticket rounded-2xl lg:rounded-3xl">
<div class="qms-display__ticket-accent" aria-hidden="true"></div>
<div class="qms-display__ticket-body">
<div class="qms-display__ticket-meta">