- inventory_use API now returns product_shopping_name in response
- showLowStockBringPrompt: uses generic shopping name (e.g. Affettato) as
Bring! item name, specific product name + brand as specification field
- addLowStockToBring: reads from window._lowStockName instead of arg
- Auto-add on depletion JS fallback: same generic-name pattern
- Deduplication check now tries both shoppingName and raw name
1. shopping_name compound-phrase map (computeShoppingName)
Add phraseMap checked against the full product name BEFORE the single-token
keyword loop. Prevents 'pane grattugiato' → 'Pane', 'panna da cucina' → 'Panna', etc.
Key new phrases:
- pane/pan grattugiato → Pangrattato
- panna da cucina / panna cucina / panna chef → Panna da cucina
- fette biscottate → Fette biscottate
- aceto balsamico / glassa balsamico → Aceto balsamico
- latte condensato/evaporato/vegetale/di soia/mandorla/avena/riso/cocco → specific
- prosciutto cotto → Prosciutto cotto
- farina di riso/mais/integrale → specific
- pasta fresca, zucchero di canna, acqua minerale/frizzante/gassata, brodo, …
Also added single-token safety-net entries: 'grattugiato'/'grattato'/'pangrattato'
→ 'Pangrattato', 'biscottate' → 'Fette biscottate'.
2. DB migration (sqlite3 UPDATE)
Re-classified 10 products that had wrong shopping_name:
Pane grattugiato → Pangrattato
Panna da cucina (×4) → Panna da cucina
Fette biscottate (×2) → Fette biscottate
Aceto balsamico (×3) → Aceto balsamico
Cleared 2 stale Gemini cache entries.
3. showLowStockBringPrompt (app.js)
When totalRemaining <= 0 (product fully depleted), skip the modal entirely.
The backend already auto-adds to Bring! on depletion; the JS only asks as a
fallback if that failed (fire-and-forget async, never blocks the UI).
The afterCallback (e.g. move-remainder modal, navigate to dashboard) is called
immediately without user interaction.
On Android WebView, window.speechSynthesis.getVoices() often returns empty
because the Web Speech API cannot enumerate the device's TTS voices.
This caused the kiosk to show 'nessuna voce offline è supportata'.
Changes:
- KioskActivity.kt: initialise Android TextToSpeech engine on startup;
expose speak(text, rate, pitch), stopSpeech() and isTtsReady() via
the existing _kioskBridge JavascriptInterface; release TTS in onDestroy.
- app.js (_speakBrowser): when _kioskBridge.speak is available, delegate
to it instead of using speechSynthesis — works even without offline voice
packs installed.
- app.js (_initBrowserTtsVoices): show 'Voce nativa Android (kiosk)'
in the voice dropdown when running inside the kiosk WebView.
- app.js (testTTS): use the bridge path when testing TTS inside the kiosk.
- Extended keyword map: 100+ entries covering bread (bauletto->Pane),
cheese (bel->Formaggio, casatella->Formaggio), wine (vesoletto/trebbiano->Vino),
foreign brand names (kaffee->Caffe, risofrolle->Cracker, zuppalatte->Biscotti,
inchusa->Birra, apfelsaft->Succo, kartoffelpüree->Purè, ciobar->Cioccolata calda,
ovomaltine->Bevande), desserts (gelato->Gelato), herbs (camomilla->Camomilla),
liquors (sambuca->Liquore), sugar variants (zuccheri->Zucchero), foreign words
(jus/zumo/arome->Italian equivalents)
- Add _geminiClassifyProduct(): calls Gemini 2.0 Flash for ambiguous product names,
with persistent cache in data/shopping_name_cache.json (never re-queries same product)
- computeShoppingName() now calls Gemini when keyword map and Bring! catalog both fail
and the product name is multi-token or has a brand/category hint
- saveProduct() auto-computes shopping_name on every create/update (already in prev commit)
- DB migration: all 210 products re-classified with new rules
- shopping list: 38->33 groups (Formaggio +4v, Affettato +5v, Biscotti +1v, Pane +1v...)
- Final SQL fixes for edge cases: Gelato, Camomilla, brand name single tokens
- Add shopping_name column to products table
- Add computeShoppingName() PHP auto-assign function:
* Curated keyword map: all salumi/cold cuts → 'Affettato'
* Bring! catalog back-translation: 'Latte di Montagna' → 'Latte'
* Fallback: first significant token capitalized
- Migrate all 210 existing products with auto-computed shopping_name
- saveProduct() auto-computes shopping_name on every create/update
- smartShopping() groups items by shopping_name: most urgent item is
representative, others listed as variants (e.g. 'Affettato' shows
Mortadella, Speck, Nduja, Salame, Prosciutto, Schinkenspeck as one row)
- _productOnBring() also checks shopping_name for Bring! match detection
- addToInventory auto-remove: uses shopping_name-based Bring! key
- useFromInventory auto-add: sends shopping_name to Bring! (not raw name),
specific product name goes into specification field
- Frontend renderSmartItem: shows shopping_name as title, specific
product name(s) in italic subtitle line below
- _syncOnBringFlags: matches on both name and shopping_name
getFinishedItems now:
- Computes total_in - total_out for every qty=0 row
- If balance <= unit threshold (e.g. <20g, <0.1 conf): product was
legitimately used up → silently DELETE, no banner shown
- Only if balance > threshold (unexpected zero): return to frontend
so banner asks user to verify
Banner detail now shows the expected residual qty so user understands
why the alert fired.
Of the 75 qty=0 rows restored in previous commit, delete the 66 where
total_in - total_out <= unit threshold (legitimately finished by user).
Keep only 9 products where transaction math says there should still be
stock but inventory hit 0 (likely system/scale error):
- Latte di Montagna (0.41 conf)
- Passata di pomodoro (692g)
- Carote (80g), mele (6pz), Uova biologiche (1pz)
- Cipolla dorata (496pz), Panna da cucina (0.6 conf)
- Gran bauletto integrale e noci (448 conf), il tuo muesli mountain (331g)
These 9 will appear in the banner asking user to verify.
- useFromInventory: replace DELETE with UPDATE qty=0 when stock hits 0
(both normal path and use-all-locations path)
- listInventory: add WHERE quantity > 0 so qty=0 rows are invisible in
the regular inventory list
- New API actions: inventory_finished_items (query) and
inventory_confirm_finished (delete after user confirms)
- Banner: new 'finished' type (priority 600, above anomalies)
Shows: '{name} — è finito?' with two buttons
'Sì, è finito' → permanently deletes the qty=0 row
'No, ne ho ancora' → navigates to add-inventory form
- i18n: banner_finished_* and toast.product_finished_confirmed (it/en/de)
- DB migration: restored 75 auto-deleted products (last 30 days) as
qty=0 inventory rows so they appear in the banner queue
- Retry loop extended to 10s (50×200ms) for slow Android WebViews
- Show 'Nessuna voce disponibile' after timeout instead of infinite loader
- Show 'Voce non supportata dal browser' if speechSynthesis missing
- Reset to loading state on each settings open (fixes stale empty select)
- Add refresh button ↺ to force-reload voices manually
Chrome loads voices async — getVoices() returns [] on first call
and onvoiceschanged may fire before the handler is assigned (race).
Add fallback retry loop: poll every 200ms for up to 4s.
- Scadenze: rimuove 'in scadenza' dal banner (solo prodotti già scaduti)
- Consumo anomalo: spiega la media giornaliera, giorni dall'ultimo rifornimento
e direzione della discrepanza (più/meno del previsto) con contesto
- Quantità sospetta: messaggio specifico per caso (bassa/alta/conf insolita)
- Anomalia inventario: linguaggio naturale invece di jargon tecnico
phantom='hai più scorte del previsto', missing='hai meno scorte del previsto'
- API prediction: aggiunge days_since_restock, direction, tx_count
- confirmBannerPrediction: toast con info su ricalcolo previsioni
- Add generateRecipeStream() endpoint with real-time SSE status events
- Frontend generateRecipe() uses ReadableStream for live step updates
- Fix gemini-2.5-flash thinking model: disable thinkingBudget, raise maxOutputTokens to 4096
- Passo 2 is pure PHP heuristic (zero extra AI calls)
- Retry logic with live countdown on 429, fallback chain: 2.5-flash → 2.0-flash
- Pass all ingredients when meal plan is active (no limits)
- Add recipe-loading-msg element with CSS transition
- Aggiunto helper callGemini($url, $payload, $timeout):
* Fino a 4 tentativi su 429 / 503
* Legge Retry-After header dalla risposta HTTP di Google
* Legge retryDelay dal corpo JSON di errore (es. '10s', '30s')
* Backoff default: 2s, 4s, 6s (sovrascitto da Google se specificato)
- geminiReadExpiry(), geminiChat(), geminiIdentifyProduct(): rimosso curl
diretto senza retry, ora usano callGemini()
- generateRecipe(): rimosso vecchio loop manuale (3 tentativi, 2s/4s fissi),
ora usa callGemini() che rispetta i delay suggeriti da Google
- In caso di 429 finale restituisce il messaggio di errore da Google (non generico)
- New API endpoint 'inventory_anomalies': detects items where stored qty
differs from tx history by >20% AND >50 units (phantom qty or missing qty)
- New API endpoint 'dismiss_anomaly': persists dismissal in anomaly_dismissed.json
- Banner system: new 'anomaly' type shown in dashboard alert banner
with 'Correggi' (opens edit) and 'Ok, ignora' (dismisses) buttons
- CSS: banner-anomaly style (orange gradient)
- Fix: lo zucchero azzerato (175g fantasma rimossi), aggiunto a Bring!
_backgroundBringSync() riscritto completamente:
- Aggiorna SEMPRE smartShoppingItems + shoppingItems con dati freschi
- Aggiunge item high/critical mancanti su Bring
- Aggiorna spec urgenza su item già presenti (anche downgrade)
- Rimuove automaticamente item auto-aggiunti quando non più urgenti
- Se la pagina shopping è aperta, re-renderizza i dati freschi
- Gira ogni 5 min via setInterval, non dipende da nessuna navigazione
- Aggiorna il badge urgenza dashboard in background
- Cache smart_shopping: 10min → 3min (urgenza fresca)
- backgroundBringSync: ogni 10min → ogni 5min + setInterval continuo
- backgroundBringSync: aggiunge anche 'high' (non solo 'critical') a Bring
- autoSyncUrgencySpecs: aggiorna spec anche se il livello di urgenza sale/scende
- Risultato: Latte/prodotti urgenti compaiono su Bring in max ~5min
- .htaccess: aggiunge Cache-Control no-cache/no-store per .js/.css
- Kiosk Kotlin: aggiunge cacheMode=LOAD_NO_CACHE al WebView
- Il pulsante refresh del kiosk ora carica sempre l'ultima versione
- Aggiunge iniezione overlay kiosk lato web (X chiudi + ↻ refresh) quando _kioskBridge è disponibile, più affidabile dell'iniezione Android-side
- Riduce timer stabilità bilancia da 10s a 5s
Prompt ricette:
- Lista ingredienti compatta: skip staples, no brand, flag brevi (🔴3gg)
- Cap gruppo 4 a 40 items, gruppo 6 a 20
- Regole condensate da 10 verbose a 6 concise
- Testi condizionali (varietà, regen, opzioni) abbreviati
- Aggiunto detail errore API Gemini nel toast
Bilancia:
- Ignora oscillazioni sub-grammo (jitter 0.5g)
- Confronto integer-gram prima di dichiarare instabile
- autoAddCriticalItems now bypasses _isBringPurchased for imminent items
(days_left <= 7 and uses_per_month >= 3)
- prevents missing Bring additions for items predicted to finish within a week
after temporary local blocklist entries
- italianToBring(): pass-2 whole-word fallback now ignores generic qualifiers
(dolce, light, classico, originale, etc.)
- when multiple single-word matches exist, choose the longest/specific token
instead of first catalog iteration hit
- prevents wrong mappings like 'Pancetta Dolce' being interpreted via generic
adjective rather than the core product token
- PHP: predictive urgency block now scales by imminence:
round(days_left) <= 3 → high, <= 7 → medium, <= 14 → low
(was flat 'low' for any days_left <= 14)
- PHP: also upgrades existing 'low' urgency to 'high' when
imminent depletion detected (round(days_left) <= 3, isFrequent)
- JS: autoAddCriticalItems now also adds:
- high urgency items with pct_left < 20% (nearly empty)
- high urgency items with days_left <= 3 (imminent)
- any item with days_left <= 2 and uses_per_month >= 5
Result: Latte di Montagna (27.8x/mo, 3 days left) now appears
on shopping list before running out, as do Lenticchie/Riso
Basmati at 1% stock and Sandwich at 1 day left.
- index.html: replace broken byte with 📋 emoji in Storico nav button
- submitRecipeUse: pass 'Ricetta: <title>' as notes to inventory_use API
so every ingredient use is linked to the recipe in the DB
- loadLog: render recipe note as small italic line 🍳 below log-detail row
- style.css: add .log-recipe-note style (0.75rem, muted, italic)
- Kiosk: replace header-inject overlay with position:fixed div appended to <html>
so buttons appear regardless of SPA init timing
- Kiosk: bump versionCode 3→4, versionName 1.2.0→1.3.0
- Kiosk: add explicit signingConfigs block (debug keystore) to avoid signature
mismatch on updates; update banner now shows uninstall instruction + 12s timeout
- Web: v1.4.0 → v1.5.0
- Preferred use-location: remember last N location choices per product; after 3+
consistent picks auto-select and collapse location picker (with 'cambia' link)
- Scale: call updateScaleReadButtons() on every status change so live-box and
read button appear instantly on reconnect without manual refresh
- Smart shopping cache: invalidate JSON cache file on every inventory_add and
inventory_use so next shopping-page load always sees current stock
- isLowStock: conf threshold changed <= 1 → < 1 (1 full pack is not low stock)
- italianToBring: replace substring matching with whole-word matching (min 4 chars)
to prevent 'gin' matching 'original', 'rum' matching 'crumble', etc.
Philadelphia original was silently mapped to Gin and skipped as duplicate
- Storico: add undo support (transaction_undo endpoint, undone column, JS undo btn)
- LOG → Storico rename in UI, nav, translations
- Bring! sync: urgency-aware purchased blocklist TTL (critical 30m, high 90m, others 4h)
- forceSyncBring() button to clear all guards and re-sync from scratch
- Scale live-box: position:fixed CSS class, 1.6rem/800 value, direct ml display
- Recipe use modal: scale live-box with 10s stability + 5s auto-confirm countdown
- Recipe use modal: show recipe quantity as highlighted row in Usa popup
1. AI photo scan: searches local DB for matching products and shows
'Già in dispensa' section before OFF matches. User can tap an
existing product directly. 'Non è nessuno di questi' button for
new products.
2. Scale live box: when product unit is ml, shows hint
'Peso in grammi → verrà convertito in ml' so user knows the
gram reading will be converted.
3. Scale auto-fill: ignores stable weight if it differs less than
10g from the last confirmed reading. Prevents re-triggering the
same weight when switching between products on the scale.
_scaleLastConfirmedGrams tracks the last auto-confirmed weight
and resets on page navigation.
- Exit ✕ and Refresh ↻ buttons now appear left of the title
- Refresh clears WebView cache and reloads (picks up web app updates)
- Uses native bridge hardReload() for true cache-busting reload
- Banner alerts reload automatically when dashboard is shown
- Manifest: CAMERA, RECORD_AUDIO, READ_EXTERNAL_STORAGE, READ_MEDIA_IMAGES
- Runtime: requests all permissions on startup (requestAllPermissions)
- WebView: onPermissionRequest checks runtime grants, requests if needed
- onRequestPermissionsResult grants pending WebView permission after user allows
- Camera and mic now work inside the kiosk WebView
- Removed invisible overlay that was blocking camera/Gemini buttons
- Added a visible ✕ button in the header-actions bar
- Tap shows confirm dialog 'Uscire dalla modalità kiosk?'
- All header buttons (camera, Gemini, scale) work normally again
- Added _bannerEditPending flag set when edit/weigh triggered from banner
- submitEditInventory now calls dismissBannerItem() after save
- Next banner item shows automatically after correction
- Flag reset on modal close (cancel) to prevent stale state
- Triple-tap exit zone now covers full header height (was 6px, untappable)
- Uses touchend event instead of click for reliable tablet interaction
- JS bridge registered once before loadUrl (not on every page load)
- Update banner auto-dismisses after 3 seconds
- CRITICAL: _finishSetup() no longer sends empty strings to save_settings
→ was overwriting .env values (Gemini key, Bring credentials) with blanks
→ now only sends non-empty values to the API
- Screen pinning (startLockTask) blocks home/recent buttons
- Gateway launches in background, kiosk returns to front after 1.5s
- Injected thin green bar at top of WebView for triple-tap exit
- JavaScript bridge for kiosk exit from WebView context
- Update check via GitHub releases API (every 6h)
- Shows banner in WebView when kiosk/gateway updates available
- Setup wizard no longer re-appears after completion/skip (evershelf_setup_done flag)
- REORDER_TASKS permission for moveTaskToFront
- singleTask launch mode for proper kiosk behavior
- Version bumped to 1.2.0 (versionCode 3)