Fix display voice announcements broken by poll refactor.
Deploy Ladill Queue / deploy (push) Successful in 35s

Replace Alpine-incompatible Set and stuck speaking lock with a plain-object queue; resume speechSynthesis and exempt display ack from CSRF.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-29 22:01:45 +00:00
co-authored by Cursor
parent c5c2c02e47
commit 5a8844410f
2 changed files with 51 additions and 38 deletions
+3
View File
@@ -39,6 +39,9 @@ return Application::configure(basePath: dirname(__DIR__))
}, },
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'display/*/announcements/*/played',
]);
$middleware->redirectGuestsTo(fn (Request $request) => route('sso.connect', [ $middleware->redirectGuestsTo(fn (Request $request) => route('sso.connect', [
'redirect' => $request->fullUrl(), 'redirect' => $request->fullUrl(),
])); ]));
+48 -38
View File
@@ -56,8 +56,8 @@ export function registerKioskFlow(Alpine) {
Alpine.data('displayBoard', (config = {}) => ({ Alpine.data('displayBoard', (config = {}) => ({
payload: null, payload: null,
spokenIds: new Set(), announcedIds: {},
speaking: false, isAnnouncing: false,
dataUrl: typeof config === 'string' ? config : config.dataUrl, dataUrl: typeof config === 'string' ? config : config.dataUrl,
playedUrlTemplate: config.playedUrl ?? null, playedUrlTemplate: config.playedUrl ?? null,
pollMs: config.pollMs ?? 1200, pollMs: config.pollMs ?? 1200,
@@ -67,61 +67,70 @@ export function registerKioskFlow(Alpine) {
try { try {
const res = await fetch(this.dataUrl, { headers: { Accept: 'application/json' } }); const res = await fetch(this.dataUrl, { headers: { Accept: 'application/json' } });
this.payload = await res.json(); this.payload = await res.json();
await this.handleAnnouncements(this.payload?.announcements ?? []); this.maybeAnnounce(this.payload?.announcements ?? []);
} catch (e) { } catch (e) {
console.error('Display poll failed', e); console.error('Display poll failed', e);
} }
}, },
async handleAnnouncements(announcements) { maybeAnnounce(announcements) {
if (! window.speechSynthesis || this.speaking) return; if (! window.speechSynthesis || this.isAnnouncing || ! announcements.length) {
return;
for (const item of announcements) {
if (! item?.id || ! item?.message || this.spokenIds.has(item.id)) {
continue;
}
this.speaking = true;
try {
await this.speakAnnouncement(item);
this.spokenIds.add(item.id);
await this.ackPlayed(item.id);
} finally {
this.speaking = false;
}
} }
const next = announcements.find((item) => item?.id && item?.message && ! this.announcedIds[item.id]);
if (! next) {
return;
}
this.isAnnouncing = true;
this.announcedIds[next.id] = true;
this.speakAnnouncement(next);
}, },
speakAnnouncement(item) { speakAnnouncement(item) {
const repeats = Math.max(1, Number(item.repeat_count) || 1); const repeats = Math.max(1, Number(item.repeat_count) || 1);
const volume = Math.min(1, Math.max(0, (Number(item.volume) || 80) / 100)); const volume = Math.min(1, Math.max(0, (Number(item.volume) || 80) / 100));
let remaining = repeats;
return new Promise((resolve) => { const finish = () => {
let remaining = repeats; this.isAnnouncing = false;
this.ackPlayed(item.id);
this.maybeAnnounce(this.payload?.announcements ?? []);
};
const speakOnce = () => { const speakOnce = () => {
const utter = new SpeechSynthesisUtterance(item.message); window.speechSynthesis.resume?.();
utter.volume = volume;
utter.lang = item.locale === 'fr' ? 'fr-FR' : item.locale === 'tw' ? 'ak-GH' : 'en-US'; const utter = new SpeechSynthesisUtterance(item.message);
utter.onend = () => { utter.volume = volume;
remaining -= 1; utter.lang = item.locale === 'fr' ? 'fr-FR' : item.locale === 'tw' ? 'ak-GH' : 'en-US';
if (remaining > 0) { utter.onend = () => {
speakOnce(); remaining -= 1;
} else { if (remaining > 0) {
resolve(); speakOnce();
} } else {
}; finish();
utter.onerror = () => resolve(); }
window.speechSynthesis.speak(utter); };
utter.onerror = () => {
delete this.announcedIds[item.id];
finish();
}; };
speakOnce(); window.speechSynthesis.speak(utter);
}); };
speakOnce();
}, },
async ackPlayed(id) { async ackPlayed(id) {
if (! this.playedUrlTemplate) return; if (! this.playedUrlTemplate) {
return;
}
const url = this.playedUrlTemplate.replace('__ID__', String(id)); const url = this.playedUrlTemplate.replace('__ID__', String(id));
try { try {
await fetch(url, { await fetch(url, {
method: 'POST', method: 'POST',
@@ -132,6 +141,7 @@ export function registerKioskFlow(Alpine) {
}); });
} catch (e) { } catch (e) {
console.error('Announcement ack failed', e); console.error('Announcement ack failed', e);
delete this.announcedIds[id];
} }
}, },