diff --git a/.github/workflows/build-kiosk.yml b/.github/workflows/build-kiosk.yml index a95b755..e012b16 100644 --- a/.github/workflows/build-kiosk.yml +++ b/.github/workflows/build-kiosk.yml @@ -1,5 +1,8 @@ name: Build & Release Kiosk APK +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + on: push: branches: [main] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09eab30..c5d2418 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: lint-php: name: PHP Syntax Check diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c31a59d..a257241 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,5 +1,8 @@ name: Security Scan (Trivy) +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + on: push: branches: [main, develop] diff --git a/assets/js/app.js b/assets/js/app.js index 911f6df..3b14347 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -9390,14 +9390,6 @@ function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacu return; } - // If the product only exists at fromLoc (no other active locations), there is - // nothing to move — auto-stay silently without showing the modal. - const hasOtherLocs = (_useCurrentItems || []).some(i => i.location !== fromLoc); - if (!hasOtherLocs) { - _saveVacuumAndStay(openedId || 0); - return; - } - const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc); const locButtons = otherLocs.map(([k, v]) => `` @@ -14113,22 +14105,28 @@ function _speakBrowser(text) { utt.voice = preferred; utt.lang = preferred.lang; } else { - // 2. First Italian voice as fallback (avoids silent-failure on browsers with no 'it-IT' default) - const itVoice = voices.find(v => v.lang && v.lang.startsWith('it')); - if (itVoice) { - utt.voice = itVoice; - utt.lang = itVoice.lang; - } else if (voices.length > 0) { - // 3. Any available voice - utt.voice = voices[0]; - utt.lang = voices[0].lang || 'it-IT'; + // Prefer offline (localService) voices to avoid silent failure when no internet. + // Priority: local Italian → any Italian → local any-lang → first available → lang-only + const itLocal = voices.find(v => v.lang && v.lang.startsWith('it') && v.localService); + const itCloud = voices.find(v => v.lang && v.lang.startsWith('it')); + const anyLocal = voices.find(v => v.localService); + const chosen = itLocal || itCloud || anyLocal || voices[0]; + if (chosen) { + utt.voice = chosen; + utt.lang = chosen.lang; } else { - // 4. No voices loaded yet — set lang and let the browser decide + // No voices loaded yet — set lang and let the browser decide utt.lang = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-US' : 'it-IT'; } } - // Chrome quirk: cancel() + immediate speak() is silently dropped — 50 ms gap fixes it - setTimeout(() => window.speechSynthesis.speak(utt), 50); + // Chrome quirks: + // 1. cancel() + immediate speak() is silently dropped → 50 ms gap fixes it + // 2. speechSynthesis gets paused after tab backgrounding; cancel() does NOT + // clear the paused state — need an explicit resume() before speak() + setTimeout(() => { + if (window.speechSynthesis.paused) window.speechSynthesis.resume(); + window.speechSynthesis.speak(utt); + }, 50); }; // If voices haven't loaded yet (async in Chrome/Android), wait once then speak @@ -14150,6 +14148,17 @@ function _speakBrowser(text) { } } +function testSound() { + const statusEl = document.getElementById('tts-test-status'); + _ensureAudioUnlocked(); + _playCookingTimerSound('done'); + if (statusEl) { + statusEl.style.display = 'block'; + statusEl.className = 'settings-status success'; + statusEl.textContent = '🔔 Suono inviato — hai sentito un beep?'; + } +} + async function testTTS() { const statusEl = document.getElementById('tts-test-status'); const enabled = document.getElementById('setting-tts-enabled')?.checked; @@ -14161,6 +14170,12 @@ async function testTTS() { if (engine === 'browser') { // Kiosk native TTS bridge takes priority over Web Speech API if (typeof _kioskBridge !== 'undefined' && typeof _kioskBridge.speak === 'function') { + // Diagnostic: check if Android TTS engine is ready + const ready = typeof _kioskBridge.isTtsReady === 'function' ? _kioskBridge.isTtsReady() : 'unknown'; + if (ready === 'false') { + if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '❌ Android TTS non inizializzato — riavvia l\'app kiosk o installa un motore TTS dal Play Store.'; } + return; + } const s = getSettings(); s.tts_rate = parseFloat(document.getElementById('setting-tts-rate')?.value) || 1; s.tts_pitch = parseFloat(document.getElementById('setting-tts-pitch')?.value) || 1; @@ -14173,15 +14188,60 @@ async function testTTS() { if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '❌ Web Speech API non supportata da questo browser.'; } return; } + // ── Audio beep test (AudioContext — works even if TTS is broken) ───── + _ensureAudioUnlocked(); + _playCookingTimerSound('done'); // Temporarily apply form values for the test const s = getSettings(); const voiceName = document.getElementById('setting-tts-voice')?.value; s.tts_voice = voiceName || s.tts_voice; - s.tts_rate = parseFloat(document.getElementById('setting-tts-rate')?.value) || 1; + s.tts_rate = parseFloat(document.getElementById('setting-tts-rate')?.value) || 1; s.tts_pitch = parseFloat(document.getElementById('setting-tts-pitch')?.value) || 1; saveSettingsToStorage(s); - _speakBrowser('Test vocale EverShelf. La sintesi vocale funziona correttamente.'); - if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status success'; statusEl.textContent = '✅ Riproduzione in corso — controlla l\'audio del dispositivo.'; } + // Diagnostic: surface problems before attempting TTS + const voices = window.speechSynthesis.getVoices(); + if (!voices.length) { + if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '❌ Nessuna voce disponibile — installa un pacchetto vocale nelle impostazioni di sistema.'; } + return; + } + // Warn if only cloud voices are available (won't work offline) + const itLocal = voices.find(v => v.lang && v.lang.startsWith('it') && v.localService); + const anyLocal = voices.find(v => v.localService); + if (!itLocal && !anyLocal) { + if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '❌ Solo voci cloud disponibili — la sintesi vocale offline richiede una voce locale installata sul dispositivo (es. Google Text-to-Speech → Scarica voci offline).'; } + return; + } + // onerror callback: update status if speak() fails + const _ttsErrHandler = (evt) => { + if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '❌ Errore TTS: ' + (evt.error || 'sconosciuto') + ' — prova a riavviare il browser o a cambiare voce.'; } + }; + // Temporarily hook onerror via a custom utterance + const testUtt = new SpeechSynthesisUtterance('Test vocale EverShelf. La sintesi vocale funziona correttamente.'); + testUtt.rate = s.tts_rate; + testUtt.pitch = s.tts_pitch; + const chosenVoice = s.tts_voice ? voices.find(v => v.name === s.tts_voice) : null; + const fallbackVoice = itLocal || voices.find(v => v.lang && v.lang.startsWith('it')) || anyLocal || voices[0]; + const testVoice = chosenVoice || fallbackVoice; + if (testVoice) { testUtt.voice = testVoice; testUtt.lang = testVoice.lang; } + testUtt.onerror = _ttsErrHandler; + testUtt.onstart = () => { + if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status success'; statusEl.textContent = '✅ Voce attiva: ' + (testVoice ? testVoice.name + ' (' + testVoice.lang + (testVoice.localService ? ', offline' : ', cloud') + ')' : 'default'); } + }; + window.speechSynthesis.cancel(); + setTimeout(() => { + if (window.speechSynthesis.paused) window.speechSynthesis.resume(); + window.speechSynthesis.speak(testUtt); + // If onstart doesn't fire within 2s, show a warning + setTimeout(() => { + if (statusEl && statusEl.className.includes('success')) return; // already started + if (!statusEl?.className.includes('error')) { + statusEl.style.display = 'block'; + statusEl.className = 'settings-status error'; + statusEl.textContent = '❌ Nessuna risposta dalla voce — se il beep era udibile, il TTS è bloccato. Prova a ricaricare la pagina o a cambiare voce.'; + } + }, 2000); + }, 50); + if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status'; statusEl.textContent = '🔊 Beep + TTS in corso...'; } return; } // Server engine diff --git a/index.html b/index.html index f1d000f..c19778e 100644 --- a/index.html +++ b/index.html @@ -1314,6 +1314,7 @@ +
diff --git a/translations/de.json b/translations/de.json index 96b1999..b631603 100644 --- a/translations/de.json +++ b/translations/de.json @@ -693,6 +693,7 @@ "extra_fields_label": "➕ Zusätzliche Felder (JSON)", "extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}", "extra_fields_hint": "Zusätzliche Felder im Payload, im JSON-Format. Leer lassen wenn nicht benötigt.", + "test_sound_btn": "🔔 Klangtest ausführen", "test_btn": "🔊 Testansage senden", "voices_loading": "Stimmen werden geladen…", "voice_not_supported": "Stimme vom Browser nicht unterstützt", diff --git a/translations/en.json b/translations/en.json index 684f849..b33856f 100644 --- a/translations/en.json +++ b/translations/en.json @@ -693,6 +693,7 @@ "extra_fields_label": "➕ Extra fields (JSON)", "extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}", "extra_fields_hint": "Additional fields to include in the payload, in JSON format. Leave empty if not needed.", + "test_sound_btn": "🔔 Run Sound Test", "test_btn": "🔊 Send Test Voice", "voices_loading": "Loading voices…", "voice_not_supported": "Voice not supported by this browser", diff --git a/translations/it.json b/translations/it.json index c9ba5b9..3bbdb82 100644 --- a/translations/it.json +++ b/translations/it.json @@ -693,6 +693,7 @@ "extra_fields_label": "➕ Campi extra (JSON)", "extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}", "extra_fields_hint": "Campi aggiuntivi da includere nel payload, in formato JSON. Lascia vuoto se non necessario.", + "test_sound_btn": "🔔 Esegui Test Suono", "test_btn": "🔊 Invia Test Vocale", "voices_loading": "Caricamento voci…", "voice_not_supported": "Voce non supportata dal browser",