Products like salt/spices that are never marked per-use now get
consumption rate estimated from the average time between restocks:
avgCycleDays = (lastIn - firstIn) / (buyCount - 1)
estimatedDaysLeft = avgCycleDays - daysSinceLastBuy
Requirements: buyCount >= 3, dailyRate == 0, avgCycle >= 7 days.
Appears in smart shopping list with reason 'Finisce tra ~Ngg (ciclo medio Mgg)'.
Also marks buy-cycle products as isRegular so stock checks apply.
- Add _getFpCachePath(), _loadFpCache(), _saveFpCache() helpers
- Check data/reported_issue_fps.json before GitHub Search API
(falls back to /tmp/ when data/ is not writable)
- Save new issue number to cache immediately after creation
- Apply 30-minute comment throttle per fingerprint
- Fall back to GitHub Search on first run / cache miss
Fixes root cause of ~50 duplicate issues (#134 duplicates #135-#183)
caused by GitHub Search API indexing delay.
- HA sensor: expiring_list now includes full product details (location, brand,
category, days_remaining, opened_at, vacuum_sealed, default_quantity, etc.)
- HA sensor: new expired_list attribute with full product details per expired item
- HA sensor: new low_stock_list attribute (items with quantity ≤ 1, full details)
- HA sensor: new sensor=product endpoint (?action=ha_sensor&sensor=product)
with optional filters: &id=, &name=, &location=
- HA cron webhook: expiry alert items now carry full product details
- Inventory edit: confirm dialog when quantity exceeds unit-specific threshold
(prevents data loss from unit-confusion typos, e.g. 183 conf instead of 0.183)
- Recipe AI: explicit rule against ingredient form substitution
(fresh tomatoes ≠ passata, fresh milk ≠ UHT ≠ cream, etc.)
- Shelf-life: opened bread rules (piadina 2d, bauletto/pancarrè 4d, pane 3d)
- docs/wiki: HA page updated with new schema, examples, product endpoint
Closes#125
- #124: star toggle on recipe view + favorites shown first in archive with gold border
- #123: +/- persons buttons on recipe to scale ingredient quantities
- #117: wasted value in EUR displayed in monthly stats section
- #118: macronutrient breakdown panel (P/C/F/fiber bars) with 4th insight rotation phase
- DB: is_favorite column on recipes, nutriments_json on products (auto-migrated)
- OFF API: nutriments fields fetched and stored per product
- Translations: it/en/de/fr/es updated with new keys
- _startInsightAlternation: 3_600_000ms → 30_000ms so rotation is actually
visible without waiting an hour
- _applyInsightPhase: smart phase-skip instead of always falling back to waste;
if current phase has no content, advance to the next non-empty phase
- PHP: add _normalizeCat() mapping OpenFoodFacts slug format ('en:dairies',
'en:plant-based-foods-and-beverages', …) to Italian app categories; also
re-aggregates counts after normalization so 'en:dairies' + 'en:milk' both
count toward 'latticini' correctly
- JS _renderMonthlyStatsSection: guard t() fallback — if t() returns the key
itself (not found), display a cleaned-up slug instead of the raw key
Products used ≥ 4x/month (daily staples: latte, pane, uova) now appear in
the smart shopping list when stock will deplete within 28 days (instead of 14).
Products used ≥ 2x/month (weekly staples: yogurt, frutta, carne) appear
within 21 days. The existing ≥1.5x/month rule at 14 days is unchanged.
No manual pin or UI needed: frequency is detected automatically from
transaction history. justRestocked guard prevents noise for freshly
bought items.
- app.js: TTS kiosk timeout 4s → 10s; fires interactive 'Hai sentito?' YES/NO
prompt instead of showing error (TTS can take 6-8s; UtteranceProgressListener
may not fire on all firmware); YES → success, NO → troubleshooting steps
- translations: add heard_question/heard_yes/heard_no/test_ok_kiosk/test_fail_steps
to all 5 languages (it/en/de/fr/es) under settings.tts
- api/index.php: fix end() PHP 8.0+ reference error in _offFetchProduct()
(categories_hierarchy stored in temp var before calling end()) (fixes#130)
- api/database.php: migrateDB() now checks sqlite_master for 'products' table;
if missing, calls initializeDB() and returns — no ALTER on nonexistent table
(fixes#133, covers #131)
- api/index.php: health_check db_row_count query guarded against missing
inventory table (fixes#131)
SetupActivity:
- btnTestRetry click handler: add step3NextButtons.visibility = VISIBLE
so the Indietro/Avanti buttons reappear after pressing No-retry
(previously they stayed hidden → user was stuck with no way to go back)
- onDisconnected(): always re-enable btnTestRetry so user is never stuck
when scale drops unexpectedly before a weight reading arrives
SettingsActivity / activity_settings.xml:
- Add 'IMPOSTAZIONI AVANZATE' section explaining that HA, Gemini AI,
- Add '← Torna all'app per le impostazioni avanzate' button (finish())
Android TTS was silently failing because engine.speak() used the default
audio stream, which may be muted while the media stream is not.
Changes:
- KioskActivity: speak() now passes Bundle with KEY_PARAM_STREAM=STREAM_MUSIC
so TTS plays on the same channel as beep/media audio
- KioskActivity: add UtteranceProgressListener that calls window._kioskTtsDone()
or window._kioskTtsError(uid, code) back into the WebView on completion/failure
- app.js testTTS(): kiosk path now shows real result from Android callbacks
(success/error) with a 4s fallback timeout showing actionable troubleshooting hints
Add FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true env var to ci.yml,
security.yml and build-kiosk.yml to prevent action failures when
GitHub removes Node 20 runner support on 2026-06-02.
Also re-triggers CI to bypass transient 403 infrastructure failure
that hit the previous run (GitHub CDN outage).
- 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.