- Add testSound() function: plays AudioContext beep independently of TTS
- Add '🔔 Esegui Test Suono' button in TTS settings (above voice test btn)
- Add translations: settings.tts.test_sound_btn (it/en/de)
- Remove hasOtherLocs guard in showMoveAfterUseModal: always show
location-choice modal when opening a package, regardless of whether
the product already exists in other locations (e.g. peas only in pantry
→ user can now choose to move opened portion to fridge)
_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
- PHP (all 3 recipe endpoints): conf products with weight/volume package_unit
(e.g., 300g/conf) now keep qty_number in sub-units (grams/ml) instead of
converting to fractional conf. Post-normalisation block converts any AI-
returned fractional conf values to grams automatically.
- JS submitRecipeUse: vacuum state now read from actual inventory item at the
used location (_recipeUseContext.items), not from recipe ingredient data.
- JS showRecipeMoveModal: now uses _prefMoveLocCache preference system —
after 2 consistent choices the modal is skipped automatically. 'Stay' button
records the choice. Added _recipeMoveCancelStay() helper.
- JS confirmRecipeMove: records move choice via _recordMoveLocChoice();
accepts optional forcedVacuum param for preference-triggered auto-moves.
Closes#108 (duplicate of #107 — data dir permissions)
- getSettings() no longer reads from localStorage; uses _settingsCache only
- saveSettingsToStorage() no longer writes to localStorage
- _applySyncedSettings() no longer writes to localStorage
- syncSettingsFromDB() meal_plan/tts_voice blocks no longer write to localStorage
- loadSettingsUI() server-merge block no longer writes to localStorage
- flipCamera() saves camera_facing directly to server via _saveSettingToServer()
- New helper _saveSettingToServer(data): calls save_settings API for partial updates
- onShoppingEnabledChange() / onShoppingModeChange() now immediately persist to
server .env via _saveSettingToServer() — no wait for Save button
- Early-theme IIFE: reads dedicated evershelf_dark_mode key (falls back to old
evershelf_settings for backward compat) — only dark_mode kept in localStorage
as a technical necessity for pre-render theme application
- New shopping_list SQLite table (migration in migrateDB)
- shoppingGetList/Add/Remove — delegates to Bring! or internal DB
based on SHOPPING_MODE env var (default: internal)
- isShoppingBringMode() guard: requires mode=bring + BRING credentials
- bringQuickSyncProduct updated to support both modes
- All bring_* JS calls replaced with shopping_* (bring_migrate_names kept)
- New settings tab 'Lista spesa' (tab-bring) with:
- Enable/disable shopping list toggle
- Provider radio: internal vs Bring!
- Bring! sub-section (shown only when mode=bring)
- AI smart suggestions toggle
- Forecast toggle
- Auto-add threshold (qty slider)
- Price estimation section
- _applyShoppingSettingsUI, onShoppingEnabledChange, onShoppingModeChange
- SHOPPING_* env vars documented in .env.example
- cron_smart_shopping respects SHOPPING_MODE and SHOPPING_SMART_SUGGESTIONS
- Translations: 12 new keys in all 5 languages (it/en/de/fr/es)
- DB busy_timeout=5000ms + WAL pragma in getDB() (fixes#95)
- Switch redirect URI from server IP to http://localhost (works everywhere)
- Add manual code exchange flow: user copies URL from browser, pastes in app
- New PHP action gdrive_oauth_exchange to exchange auth code for refresh token
- Fix null bug in gdrive_oauth_exchange (was read before initialization)
- Add #gdrive-code-section UI with input + submit button in index.html
- Update _gdriveAuthorize() to show code section and store redirect_uri
- Add _gdriveSubmitCode() JS function for manual code submission
- Update setup wizard and backup tab to show http://localhost as redirect URI
- Add 5 new translation keys (gdrive_redirect_uri_hint, gdrive_code_title,
gdrive_code_hint, gdrive_code_submit, gdrive_code_empty) in all 5 languages
- Update gdrive_oauth_steps in all translations to reflect new flow
- Document Google Drive OAuth setup in README.md
- Dark mode: comprehensive fix for 30+ components with hardcoded light colors
- api/index.php: auto-delete legacy dispensa.db when evershelf.db exists
and dispensa.db is empty (<1KB); vacuum-sealed items only show as
expired after VACUUM_EXPIRY_EXTENSION_DAYS (default 30) past printed
date; add dbCleanup() function; add recipe/tx/vacuum params to
getServerSettings + saveSettings intMap; add 'db_cleanup' action
- api/cron_smart_shopping.php: run dbCleanup() each cron cycle
- app.js: add zerowaste_tips_enabled + screensaver_timeout + retention
days to saveSettings POST payload (were missing, causing reset on sync);
asset version bumped to v=20260518b
- .env: added ZEROWASTE_TIPS_ENABLED, RECIPE_RETENTION_DAYS=7,
TRANSACTION_RETENTION_DAYS=7, VACUUM_EXPIRY_EXTENSION_DAYS=30
Backend (api/index.php):
- callGemini() now extracts usageMetadata (tokens_in/tokens_out) from response
- _recordAiUsage() persists monthly token data to data/ai_usage.json
- callGeminiWithFallback() accepts $usageAction param; all 15 call sites labeled
- gemini_usage endpoint: returns token stats, cost estimate, log info, DB size
- smartShopping(): rolling 90-day EWMA (70% last-30d / 30% days-31-90)
with fallback to all-time rate when <14 days of history
Frontend (index.html + app.js):
- New Info tab (ℹ️) in Settings with Gemini usage and System cards
- _loadInfoTab() / _renderInfoTab(): loads on click, auto-refreshes every 30s
- switchSettingsTab() stops auto-refresh when leaving Info tab
Translations (it/en/de): settings.info.* keys
Banner prodotti aperti:
- Rimosse le opzioni 'Usa comunque' e 'Ignora' (non hanno senso
se il prodotto è solo aperto — rimangono solo 'L\''ho finito!',
'L\''ho buttato', 'Correggi data')
- Per prodotti scaduti non aperti il comportamento rimane invariato
Preloader startup check:
- Sostituito il mini-label monospace con una ruota 3D (stile cooking wheel)
- Testo grande, colorato: VERDE=ok, ARANCIONE=warning, ROSSO=errore
- Il check precedente sale in cima (rotateX tilt, dimmed) mentre il
nuovo entra dal basso con animazione 3D
- setProgress() ora guida la ruota; slowAnim() aggiorna solo la barra
Defaults / non-bloccante:
- Gemini API key non impostata → ok:true 'non configurata' (verde)
- Bring! token non ancora generato → ok:true (verde, auto-generato al 1° accesso)
- La configurazione mancante mostra ✅ informativo, non ⚠️ warning
Step 5 del wizard ora mostra 4 toggle (pre-compilati se già configurati):
- Salvaschermo orologio (screensaver_enabled)
- Prezzi lista spesa (price_enabled)
- Piano pasti (meal_plan_enabled)
- Suggerimenti zero-waste durante cottura (zerowaste_tips_enabled)
Solo i toggle NON ancora impostati in prefs partono da false (fresh install).
Tutti e 4 vengono salvati in SharedPreferences e inviati al server via
save_settings al completamento del wizard.
PHP/JS: zerowaste_tips_enabled aggiunto come impostazione server-side
(ZEROWASTE_TIPS_ENABLED in .env), sincronizzata nel WebView via
_applySyncedSettings() al caricamento.
- health_check: use evershelf.db (not dispensa.db); auto-migrate if needed
- removed dispensa.db (legacy, obsolete)
- backups check: verify files exist (not dir writability, cron writes as root)
- bring_token: read data/bring_token.json (not env var)
- warning popup: 5s countdown bar with label+hint per warning, auto-closes
- error popup: blocking panel with title + hint per critical failure
- db_legacy check: warns if old dispensa.db still present
- 32 total checks (added db_legacy, tts_url, scale_gateway)
- hint messages on every check explaining cause and fix
- translations: added check_db_legacy, check_tts, check_scale,
critical_error_intro, error_network_detail in it/en/de
- Replace banner checklist with real-time progress bar + per-check label
Bar fills smoothly (0→100%) as each check runs; label shows current check.
On success: bar stays green briefly then fades. On warnings: amber badges
shown for 2.2s. On critical error: bar turns red + error block + Retry.
- Extend health_check to 29 comprehensive checks:
PHP 8.0+ version, 4 critical extensions (pdo_sqlite/curl/json/mbstring),
4 optional extensions (openssl/fileinfo/zip/intl), PHP memory/timeout/upload,
data/ writable, rate_limits/ dir, backups/ dir, actual file-write test,
free disk space, SQLite connect, required tables, PRAGMA quick_check integrity,
WAL mode, DB file size, inventory row count, .env file, Gemini AI key,
Bring! credentials + token, cURL SSL version, internet reachability (Gemini API)
- Fresh-install detection: if dispensa.db not found + data/ writable → OK (auto-create)
- Translations: startup.* expanded to 28 keys in IT, EN, DE, FR, ES
- CSS: new .preloader-progress-wrap, .preloader-bar-track, .preloader-bar,
.preloader-check-label, .preloader-warn-badge; removed old .preloader-checks
- Version: v1.7.21, assets v=20260520b
- Add ?action=health_check PHP endpoint (early-exit, before rate-limiter)
Checks: PHP version, required extensions, data/ writability, SQLite DB
connection + table integrity, .env file, Gemini AI key, Bring! token
- Display animated checklist in splash screen with per-item icons
(ok/warn/error); critical failures block app launch with clear error
message and Retry button; optional warnings shown but don't block
- New JS: _runStartupCheck(), _startupRetry(); called first in _initApp()
- New HTML elements in #app-preloader: #preloader-checks, #preloader-error-msg,
#preloader-retry-btn (hidden until startup check completes)
- New CSS: .preloader-checks, .preloader-check-row, .preloader-error-msg,
.preloader-retry-btn with state colors (ok=green, warn=amber, error=red)
- Translations: startup.* keys (10 per language) in IT, EN, DE, FR, ES
- Asset version bump: v=20260520a