Two bugs in the migration function:
1. DELETE endpoint does not exist in Bring! API — must use PUT with
'remove' param (same as the remove-from-list flow elsewhere in the code)
2. Items were added using the Italian shopping_name as the 'purchase' field
instead of the German catalog key via italianToBring(shoppingName).
This created Italian/German duplicates (e.g. both 'Affettato' and
'Aufschnitt' in the list at the same time).
Also add a pre-add duplicate check so existing catalog-key items are not
double-added when the old specific item is removed.
Manual cleanup run: removed 25 stale/duplicate items, added 8 correct
German-key items, ran migration (1 more migrated). List is now clean.
- bringGetList() now runs bringMigrateNamesInternal() silently after
returning the response, max once per 10 minutes (bring_migrate_ts.json flag)
- Refactored migration into bringMigrateNamesInternal() reusable function
- Removed manual migrate button from UI (not needed, it's automatic now)
- One-shot migration already executed via curl: 16 items updated in Bring!
- New API action bring_migrate_names: reads current Bring! list, matches
items against products DB, replaces specific names with shopping_name
(e.g. 'Mortadella IGP' → 'Affettato' with spec 'Mortadella IGP · Brand')
- New button in Bring! settings: 'Generalizza nomi lista Bring!'
with live status feedback (migrated / skipped / errors count)
- Auto-refreshes shopping list view after migration
Root cause: after scale auto-confirm fires submitUse(), the old code called
_cancelScaleAutoConfirm(false) which reset _scaleLastConfirmedGrams to null.
This allowed the scale (still showing the same reading) to start a new 10-second
stability+confirm cycle and trigger a second identical deduction.
JS fix:
- submitUse() now calls _cancelScaleTimersOnly() instead of
_cancelScaleAutoConfirm(false), preserving _scaleLastConfirmedGrams so the
same weight is rejected until the product is removed from the plate.
- _scaleStabilityVal reset to null so a genuinely new weight starts fresh.
- Duplicate API response (result.duplicate) silently ignored in the UI.
PHP fix (server-side safety net):
- useFromInventory() rejects a second 'out' transaction for the same product
within 12 seconds with { success: false, duplicate: true }.
This catches any client-side edge cases regardless of scale timing.
- 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.
- 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.
- 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
- 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!
- 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
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
- 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.
- 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
- api/scale_discover.php: async TCP scan of whole /24 subnet on port 8765,
confirms with WebSocket handshake, returns found ws:// URLs in ~1.5s
- index.html: '🔍 Auto' button next to gateway URL field
- app.js: discoverScaleGateway() — calls relay, fills URL field and
auto-saves settings + reconnects on success
- showMoveAfterUseModal: add '🫙 Torna sotto vuoto' checkbox (default=previous state)
only shown if item was vacuum sealed; confirmMoveAfterUse passes vacuum_sealed to API
- showRecipeMoveModal: same vacuum checkbox with wasVacuum default passed from ingredient
- confirmRecipeMove: passes vacuum_sealed to inventory_update
- PHP API: add vacuum_sealed to recipe ingredient enrichment
- renderCookingStep: remove step-text word filter; show ALL unused from_pantry
ingredients at every cooking step (AI uses pronouns like 'tagliarla' instead of
repeating the ingredient name, causing them to be invisible)
- Add evershelf-scale-gateway/ Android app (Kotlin):
- BLE scanning and GATT connection to smart scales
- Supports BT SIG Weight Scale (0x181D), Body Composition (0x181B), and generic heuristic parser
- WebSocket server on port 8765 (local LAN)
- Real-time weight broadcasting to EverShelf browser client
- Add scale status indicator in header (green/orange/grey dot)
- Add Settings tab for scale configuration (URL, enable toggle, test, APK download link)
- Add 'Read from scale' button in Add/Use forms when unit is g or ml
- Add scale WebSocket client logic in app.js with auto-reconnect
- Fix recipe suggestion: expiry-prioritized ingredients now only injected into
AI prompt when user explicitly selects 'Priorità Scadenze' or 'Zero Sprechi'
- Update README with smart scale section and website link
- Update all translations (it, en, de) with scale strings
PHP getStats() opened section:
- Primary detection: opened_at IS NOT NULL (reliable, set by useFromInventory)
Fallback: fractional-qty pattern (legacy items)
- Per-item compute opened_expiry = min(opened_at + estimateOpenedExpiryDaysPHP, original_expiry)
→ vacuum_sealed items get 1.5× multiplier
→ always take sooner of 'opened shelf life' vs 'original sealed expiry'
- Add days_to_expiry, opened_expiry, is_edible, has_opened_at to each item
- Filter legacy items (no opened_at) with expiry > 14 days (too much noise)
- Sort by days_to_expiry ASC (soonest/spoiled first) instead of updated_at DESC
JS dashboard opened render:
- Expiry badge: ⛔ Scaduto / ⚠️ Scade oggi / ⏰ Xgg (urgent≤2, soon≤5, ok>5)
- 🔒 icon added when vacuum_sealed=1
- Spoiled items shown with strikethrough name + muted styling (.alert-item-spoiled)
- Cap display at 10 items; 'e altri N prodotti aperti...' note if more
- Sort comes from server (removed JS openedFraction sort)
CSS:
- .opened-expiry-{ok,soon,urgent,today,spoiled} badge classes
- .alert-item-spoiled strikethrough styling
- .alert-more-note
Root cause: autoAddCriticalItems used stale in-memory cache (old critical items)
and re-added items to Bring right after manual removal, because on_bring was
now false but urgency was still 'critical' in the old cache.
PHP smartShopping():
- Rename stockByFirstToken → stockByAnyToken (indexes ALL significant tokens)
- 'Passata di pomodoro' depleted + 'Polpa di pomodoro' in stock → share token
'pomodoro' → passata no longer flagged as critical (COVERS: passata/polpa/pelato
and any future tomato product variant)
- Same logic: 'aglio'/'aglio rosso', 'latte'/'latte di montagna', etc.
JS loadSmartShopping():
- When critical item set changes (items added OR removed), immediately reset
_autoAddedCriticalTs and _bringCleanupTs so next shopping load uses fresh data
instead of debounced old data
JS cleanupObsoleteBringItems():
- Use any-token matching (like PHP) for both stockByAnyToken and urgentSmartByToken
→ 'Passata di pomodoro' in Bring, 'polpa' in stock → share 'pomodoro' → removed
BRING! REMOVAL FIX (latte/aglio not removed after shopping):
- PHP addToInventory: replace exact strcasecmp with token-based fuzzy
matching (same logic as _productOnBring) so custom Bring item names
and translated catalog keys both match correctly
- JS submitAdd: add client-side fallback — if PHP removal missed the item,
use _findSimilarItem against the loaded shoppingItems and call bring_remove
MULTI-EXPIRY BATCHES (when buying conf with different expiry dates):
- Add form (unit=conf): shows '+ Lotto con scadenza diversa' button
- Each extra batch has its own qty + expiry date input with +/- controls
- On submitAdd, extra batches are submitted as additional inventory_add calls
(separate DB rows, separate expiry dates)
- Multi-batch section hidden in 'Ce l'avevo già' mode and for non-conf units
- Re-shown/hidden when switching unit via onAddUnitChange
RECIPE COOKING STEPS - FIFO ingredient display:
- renderCookingStep: each ingredient row now shows brand chip, location chip,
and expiry date chip (color-coded: red ≤3d, yellow ≤7d)
- PHP already selected earliest-expiry inventory entry (ORDER BY days_left ASC
with > not >= ensures first/earliest match wins)
- CSS: .cooking-ing-meta, .cooking-ing-chip, .exp-soon, .exp-close,
.multi-batch-row, .multi-batch-qty, .multi-batch-date, .btn-icon-sm
- PHP smart_shopping: add absolute stock fallback that flags conf/pz items
with <=2 units (medium) or <=1 unit (high) and g/ml at <=20% of default,
regardless of usage frequency. Fixes products like Panna da cucina that
are rarely used but running low and were invisible to the frequency-based
urgency logic (pctLeft was 66% since last purchase was 3 at once).
- JS isLowStock(): return true (not false) when totalRemaining <= 0.
A fully depleted item is definitely low-stock; the Bring! add prompt
should fire when you use the very last unit.
- PHP: new 'expiry_history' action computes avg shelf life (expiry_date - added_at)
from inventory table for the same product_id (last 730 days, valid entries only)
- JS: _fetchExpiryHistoryAndUpdate() fires async after showAddForm() renders
and replaces the rule-based estimate with the historical average if available
- Labeled with '📊 storico' badge on the estimate line (tooltip shows sample count)
- recalculateAddExpiry() and selectPurchaseType('new') both honour window._historyExpiryDays
- Vacuum-sealed multiplier still applied on top of historical base
- Falls back silently to rule-based estimateExpiryDays when no history exists
- HTML: removed kg/l options from all unit selector dropdowns
- JS detectUnitAndQuantity(): auto-converts kg→g (*1000) and l→ml (*1000)
- JS unit labels: removed all kg/l entries from unitLabels maps
- JS category defaults: frutta 1000g, verdura 500g, bevande 1000ml
- JS step/min logic: simplified for g/ml only (no more 0.01 steps)
- JS getSubUnitStep(): removed kg/l cases
- JS isLowStock(): removed kg/l threshold
- JS spec parser: labels now show g/ml instead of kg/L
- PHP recipe parser: converts kg→g and l→ml immediately on parse
- PHP AI prompt: updated to specify only g/ml/pz/conf units
- PHP migration endpoint available at ?action=migrate_units (no-op if DB already clean)
- Products bought only once (buy_count=1) and out of stock are skipped entirely
(not enough history to predict repurchase need, e.g. piadelle)
- usesPerMonth for items tracked 7-30 days now normalized instead of using raw count
- usesPerMonth for items tracked <7 days halved to avoid inflation
- Critical urgency now requires buy_count >= 2
L'algoritmo ora calcola:
- usesPerMonth: utilizzi al mese (non piu solo useCount totale)
- daysSinceLastUse: giorni dall'ultimo utilizzo
- isFrequent: >= 1.5 utilizzi/mese
- isRegular: >= 0.5 utilizzi/mese (almeno 1 ogni 2 mesi)
- isRecent: usato/comprato negli ultimi 60 giorni
Nuova logica urgenze:
- CRITICAL (esaurito): solo se frequente E recente
- HIGH (esaurito): regolare E recente E storico consistente
- Quasi finito/scorta bassa: solo per prodotti regolari/recenti
- Previsione esaurimento: solo per frequenti E recenti
- Prodotti usati raramente o non di recente vengono esclusi
- Counter nei tab aggiornati dinamicamente
- Auto-aggiunta prodotti CRITICI a Bring! al caricamento (1x per sessione)
- Badge urgenza e frequenza sugli item in lista (cross-ref smart shopping)
- Tag locali per item (Urgente/Priorità/Verificare) con menu dropdown
- Ordinamento automatico per frequenza utilizzo (item più usati in cima)
- Tap su un item → scanner barcode, con banner 'Trovato! Rimuovi dalla lista'
- Fix pctLeft: usa max(1, qty) come fallback refQty per evitare falsi alert
- Fix daysLeft capped a 365gg per pulire stringhe di previsione
- Back button on action page → torna a shopping se aperto da lista