_speakBrowser: voice fallback now prefers localService=true (offline)
voices over cloud voices. On Android Chrome, cloud voices fail silently
when no internet. New priority: local Italian → any Italian → local
any-lang → first available → lang-only.
testTTS (browser engine):
- Plays _playCookingTimerSound('done') beep FIRST (AudioContext, always
works) so user can confirm audio hardware/volume is working
- Checks if only cloud voices exist → shows actionable error
- Builds utterance with onerror → shows error code on failure
- Adds onstart → shows actual voice name/lang/online status on success
- 2s watchdog: if onstart never fires → 'nessuna risposta' warning
- Shows '🔊 Beep + TTS in corso...' during the test
_speakBrowser: Chrome sets speechSynthesis.paused=true after tab is
backgrounded/minimized. Calling cancel() does NOT clear this state.
Adding resume() before speak() inside the setTimeout fixes silent
failure when user returns to the tab (e.g. switch app → back to browser).
testTTS: add diagnostic messages for:
- Android kiosk: check isTtsReady() and show actionable error if
the native TTS engine hasn't initialized yet
- Web browser: check getVoices().length before speaking; if empty,
show error asking user to install a voice pack in OS settings
(instead of always showing '✅ Riproduzione in corso' even when silent)
_speakBrowser() was silently failing in two scenarios:
1. Chrome cancel+speak bug: calling speechSynthesis.speak() immediately
after cancel() is silently dropped in Chrome. Fixed by adding a 50ms
setTimeout between cancel() and speak().
2. No voice fallback: when tts_voice preference is empty (never saved)
and the browser has no default 'it-IT' voice, speech fails silently.
Now tries: preferred by name → first Italian voice → first any voice
→ lang-only as last resort.
3. Async voice loading: Chrome/Android load voices asynchronously.
When getVoices() returns empty, now waits for onvoiceschanged (with
a 500ms safety timeout) before speaking.
CATEGORY_LABELS was built at parse time before translations were
available, causing raw keys like 'categories.verdura' to appear
in all category labels, dropdowns and inventory filters.
_applyI18nToLabels() now also:
- Rebuilds each CATEGORY_LABELS entry using the loaded i18n strings
and the corresponding CATEGORY_ICONS icon prefix
- Regenerates the static #pf-category <select> options so the
product-add form also shows translated category names
- speakCookingStep: respect tts_engine='browser' explicitly; if user
chose browser engine, always use _speakBrowser regardless of HA.
Also add per-engine try/catch so HA or server TTS failures fall back
to browser TTS instead of silently doing nothing.
- index.html: fix data-i18n='settings.tts.ha_hint' → 'settings.ha.ha_hint'
(key lived in settings.ha not settings.tts)
When user clicks 'Generate another', show a choice:
- Replace (discard current, generate new) — former behavior
- Save to archive & generate new — saves current recipe first
All 5 languages (it/en/de/fr/es) with regen_choice_title,
regen_replace, regen_save_new keys.
- Replaces the old 'New' callout with a proper dedicated section at the top of Features
- Shows all 16 sensors, 6 binary sensors, 5 buttons, todo, calendar, text, 6 services
- HACS and config_flow_start my.home-assistant.io badge buttons
Also: fix dark mode not persisting — _setThemeMode now saves immediately to server .env
- PHP generateRecipeStream: normalize recipe.steps to plain strings after
parsing Gemini JSON (handles [{text:'...'}, ...] objects gracefully)
- JS: add _stepStr(s) helper near cooking mode — safely extracts text from
a step regardless of type (string or object {text/description/step key})
and strips leading 'Passo N:' prefix in one place
- JS: replace all 7 manual step.replace(/^Passo.../) calls with _stepStr()
across renderRecipe, renderCookingStep, startCookingMode, replayCookingTTS,
toggleCookingTTS, navigateCookingStep — no more crash if Gemini schema drifts
- PHP generateRecipeStream: wrap entire body in try/catch(\Throwable) to catch
any PHP fatal/exception mid-stream and send it as a proper SSE error event
- PHP: curl timeout raised 60s→90s; capture curl errno/errmsg on failure
- PHP: HTTP error messages now include a human-readable status label
(e.g. 'Quota API esaurita (429)', 'Nessuna risposta da Gemini (cURL: ...)')
- JS catch block: show err.message alongside error.connection so the actual
JS network error (NetworkError, AbortError, etc.) is visible
- JS no-recipe+no-error path: show recipes.stream_interrupted instead of
generic error.connection
- Translation: added recipes.stream_interrupted in it/en/de
- Shopping list: each item now shows 'Hai già Xg in dispensa' for same-family inventory stock
- Lazy-loads inventory once per shopping page visit (_getShoppingInventoryCache)
- Matches by first significant token (same logic as related-stock on action page)
- Green hint below item badges, dark-mode aware (.shopping-pantry-hint)
- Barcode lookup: added Open Products Facts + Open Beauty Facts as step 3;
Gemini AI (_barcodeLookupGemini) as final step 4 fallback
- Added stockForName PHP endpoint (stock_for_name action) for future use
- Restored missing function signatures for _offFetchProduct() and saveProduct()
that were accidentally lost when stockForName was added in a previous session
- Translation: added shopping.pantry_hint in it/en/de