Fix display voice announcements broken by poll refactor.
Deploy Ladill Queue / deploy (push) Successful in 35s
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:
@@ -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
@@ -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];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user