chore: auto-merge develop → main
Triggered by: 0479e34 ci: retry after GitHub CDN outage
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
name: Build & Release Kiosk APK
|
name: Build & Release Kiosk APK
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-php:
|
lint-php:
|
||||||
name: PHP Syntax Check
|
name: PHP Syntax Check
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
name: Security Scan (Trivy)
|
name: Security Scan (Trivy)
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, develop]
|
branches: [main, develop]
|
||||||
|
|||||||
+83
-23
@@ -9390,14 +9390,6 @@ function showMoveAfterUseModal(product, fromLoc, remaining, openedId, openedVacu
|
|||||||
return;
|
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 otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc);
|
||||||
const locButtons = otherLocs.map(([k, v]) =>
|
const locButtons = otherLocs.map(([k, v]) =>
|
||||||
`<button type="button" class="loc-btn" onclick="clearMoveModalTimer();confirmMoveAfterUse(${product.id}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>`
|
`<button type="button" class="loc-btn" onclick="clearMoveModalTimer();confirmMoveAfterUse(${product.id}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>`
|
||||||
@@ -14113,22 +14105,28 @@ function _speakBrowser(text) {
|
|||||||
utt.voice = preferred;
|
utt.voice = preferred;
|
||||||
utt.lang = preferred.lang;
|
utt.lang = preferred.lang;
|
||||||
} else {
|
} else {
|
||||||
// 2. First Italian voice as fallback (avoids silent-failure on browsers with no 'it-IT' default)
|
// Prefer offline (localService) voices to avoid silent failure when no internet.
|
||||||
const itVoice = voices.find(v => v.lang && v.lang.startsWith('it'));
|
// Priority: local Italian → any Italian → local any-lang → first available → lang-only
|
||||||
if (itVoice) {
|
const itLocal = voices.find(v => v.lang && v.lang.startsWith('it') && v.localService);
|
||||||
utt.voice = itVoice;
|
const itCloud = voices.find(v => v.lang && v.lang.startsWith('it'));
|
||||||
utt.lang = itVoice.lang;
|
const anyLocal = voices.find(v => v.localService);
|
||||||
} else if (voices.length > 0) {
|
const chosen = itLocal || itCloud || anyLocal || voices[0];
|
||||||
// 3. Any available voice
|
if (chosen) {
|
||||||
utt.voice = voices[0];
|
utt.voice = chosen;
|
||||||
utt.lang = voices[0].lang || 'it-IT';
|
utt.lang = chosen.lang;
|
||||||
} else {
|
} 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';
|
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
|
// Chrome quirks:
|
||||||
setTimeout(() => window.speechSynthesis.speak(utt), 50);
|
// 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
|
// 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() {
|
async function testTTS() {
|
||||||
const statusEl = document.getElementById('tts-test-status');
|
const statusEl = document.getElementById('tts-test-status');
|
||||||
const enabled = document.getElementById('setting-tts-enabled')?.checked;
|
const enabled = document.getElementById('setting-tts-enabled')?.checked;
|
||||||
@@ -14161,6 +14170,12 @@ async function testTTS() {
|
|||||||
if (engine === 'browser') {
|
if (engine === 'browser') {
|
||||||
// Kiosk native TTS bridge takes priority over Web Speech API
|
// Kiosk native TTS bridge takes priority over Web Speech API
|
||||||
if (typeof _kioskBridge !== 'undefined' && typeof _kioskBridge.speak === 'function') {
|
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();
|
const s = getSettings();
|
||||||
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;
|
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.'; }
|
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '❌ Web Speech API non supportata da questo browser.'; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// ── Audio beep test (AudioContext — works even if TTS is broken) ─────
|
||||||
|
_ensureAudioUnlocked();
|
||||||
|
_playCookingTimerSound('done');
|
||||||
// Temporarily apply form values for the test
|
// Temporarily apply form values for the test
|
||||||
const s = getSettings();
|
const s = getSettings();
|
||||||
const voiceName = document.getElementById('setting-tts-voice')?.value;
|
const voiceName = document.getElementById('setting-tts-voice')?.value;
|
||||||
s.tts_voice = voiceName || s.tts_voice;
|
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;
|
s.tts_pitch = parseFloat(document.getElementById('setting-tts-pitch')?.value) || 1;
|
||||||
saveSettingsToStorage(s);
|
saveSettingsToStorage(s);
|
||||||
_speakBrowser('Test vocale EverShelf. La sintesi vocale funziona correttamente.');
|
// Diagnostic: surface problems before attempting TTS
|
||||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status success'; statusEl.textContent = '✅ Riproduzione in corso — controlla l\'audio del dispositivo.'; }
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// Server engine
|
// Server engine
|
||||||
|
|||||||
@@ -1314,6 +1314,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div><!-- /tts-server-section -->
|
</div><!-- /tts-server-section -->
|
||||||
|
|
||||||
|
<button class="btn btn-large btn-secondary full-width mt-2" onclick="testSound()" data-i18n="settings.tts.test_sound_btn">🔔 Esegui Test Suono</button>
|
||||||
<button class="btn btn-large btn-accent full-width mt-2" onclick="testTTS()" data-i18n="settings.tts.test_btn">🔊 Invia Test Vocale</button>
|
<button class="btn btn-large btn-accent full-width mt-2" onclick="testTTS()" data-i18n="settings.tts.test_btn">🔊 Invia Test Vocale</button>
|
||||||
<div id="tts-test-status" style="display:none;margin-top:8px"></div>
|
<div id="tts-test-status" style="display:none;margin-top:8px"></div>
|
||||||
<!-- HA TTS quick-fill hint -->
|
<!-- HA TTS quick-fill hint -->
|
||||||
|
|||||||
@@ -693,6 +693,7 @@
|
|||||||
"extra_fields_label": "➕ Zusätzliche Felder (JSON)",
|
"extra_fields_label": "➕ Zusätzliche Felder (JSON)",
|
||||||
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
|
"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.",
|
"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",
|
"test_btn": "🔊 Testansage senden",
|
||||||
"voices_loading": "Stimmen werden geladen…",
|
"voices_loading": "Stimmen werden geladen…",
|
||||||
"voice_not_supported": "Stimme vom Browser nicht unterstützt",
|
"voice_not_supported": "Stimme vom Browser nicht unterstützt",
|
||||||
|
|||||||
@@ -693,6 +693,7 @@
|
|||||||
"extra_fields_label": "➕ Extra fields (JSON)",
|
"extra_fields_label": "➕ Extra fields (JSON)",
|
||||||
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
|
"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.",
|
"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",
|
"test_btn": "🔊 Send Test Voice",
|
||||||
"voices_loading": "Loading voices…",
|
"voices_loading": "Loading voices…",
|
||||||
"voice_not_supported": "Voice not supported by this browser",
|
"voice_not_supported": "Voice not supported by this browser",
|
||||||
|
|||||||
@@ -693,6 +693,7 @@
|
|||||||
"extra_fields_label": "➕ Campi extra (JSON)",
|
"extra_fields_label": "➕ Campi extra (JSON)",
|
||||||
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
|
"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.",
|
"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",
|
"test_btn": "🔊 Invia Test Vocale",
|
||||||
"voices_loading": "Caricamento voci…",
|
"voices_loading": "Caricamento voci…",
|
||||||
"voice_not_supported": "Voce non supportata dal browser",
|
"voice_not_supported": "Voce non supportata dal browser",
|
||||||
|
|||||||
Reference in New Issue
Block a user