The wizard now detects which specific settings are missing and shows
only those steps. Existing configurations are preserved. If a future
feature adds a new required setting, it will automatically prompt for
just that one.
- Nel form USA, se il prodotto ha >=2 slot in inventario con scadenze
diverse (es. 2 lotti dispensa, o frigo+dispensa), mostra un banner
giallo: '⚠️ Usa prima quella in Frigo — scade il 12/04 (tra 3 giorni)!'
- Logica: filtra item con expiry_date, ordina per scadenza ASC,
mostra hint solo se ci sono almeno 2 scadenze diverse O 2 location diverse
- Nessun hint se tutto ha la stessa scadenza (inutile)
Ogni chiamata a showProductAction() creava un nuovo div e lo appendeva
con .after() senza rimuovere il precedente -> link moltiplicato.
Fix: il div ha id='catalog-edit-link', viene riutilizzato se esiste;
se il prodotto non è in inventario il link viene rimosso.
- Intervallo pagina corrente: 5 minuti (era 10) -> dashboard, inventario,
scadenze aperte, ricette, log si aggiornano automaticamente
- Intervallo lista spesa: 2 minuti in background anche se non sei sulla
pagina Shopping -> badge/contatore aggiornato, e se sei sulla pagina
Shopping vedi i nuovi prodotti aggiunti da Bring! da un altro device
- refreshCurrentPage() ora copre anche 'recipe' e 'log'
- visibilitychange: refresh immediato su qualsiasi pagina (già presente)
- setInterval ogni 10 minuti ricarica la pagina corrente (dashboard,
inventario, spesa) silenziosamente, senza reload — i contatori di
scadenza restano sempre aggiornati anche se la scheda è aperta da ore
- visibilitychange ora aggiorna SEMPRE la pagina corrente, non solo
la lista spesa, quando la tab torna in foreground
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
1. Use form pz step bug (banana non scalabile):
- min=0.25 + step=0.5 → solo 0.75, 1.25, 1.75... validi per browser
- 1 intero (default) era INVALIDO → form bloccato silenziosamente
- Fix: step='any', min=0.01 (il passo logico resta in adjustUseQty)
2. cleanupObsoleteBringItems mai eseguita:
- Usava products_list che non ha campo quantity → (qty||0)<=0 sempre vero → skip sempre
- Fix: usa inventory_list che ha le qty reali per location
3. cleanupObsoleteBringItems troppo rara:
- sessionStorage → una sola volta per sessione
- Fix: localStorage con TTL 30 minuti
- Ora rimuove da Bring qualsiasi item che ha scorte in inventario
e NON è flaggato come critical/high dalla spesa intelligente
The MODIFICA button on the action page was opening the catalog editor
(name/brand/category). Users expect it to edit the physical item in hand.
Changes:
- MODIFICA button → openInventoryEdit(): edits the inventory row directly.
If product is in one location → opens editActionInventoryItem directly.
If multiple locations → shows a location picker modal first.
- editActionInventoryItem modal already has: qty ±, unit, conf size, location
buttons, expiry date, vacuum toggle — all fields for the instance.
- Catalog editing (name/brand/category) moved to a small secondary link
'⚙️ Modifica scheda prodotto' shown discreetly below the action buttons.
- Removed redundant 'Tocca una riga per modificare' hint from status bar.
- Added .btn-link-small CSS class for the secondary catalog-edit link.
- Add 📷 scan button next to the barcode field in product form
Opens a camera modal (BarcodeDetector if available, manual fallback)
Detects barcode after 2 consistent frames, fills field and closes modal
- Show ⚠️ hint below the barcode field when it's empty (new products only):
'Aggiungi il barcode così al prossimo acquisto basta scansionarlo!'
Hint hides automatically when a code is entered or scanned
- Hint is hidden in edit-product mode (barcode already saved)
- scanBarcodeForForm() reuses the modal overlay; handles camera permission
errors gracefully (shows manual input only)
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.
Cooking mode TTS:
- Removed auto-speak from renderCookingStep() entirely
- TTS now fires ONLY when user presses 'Rileggi' (replayCookingTTS)
- Timer-expiry TTS unchanged (still speaks when a cooking timer expires)
submitUseAll fix:
- Changed location from selected-location to '__all__'
- 'Usato TUTTO / Finito' means the product is completely consumed;
using a specific location could fail with a 404 if the async
loadUseInventoryInfo() hadn't yet updated the selector (race condition)
- The __all__ path in PHP removes inventory across every location
When creating a new product (manual entry), a '📷 Scatta foto e identifica con AI'
button appears at the top of the form. Tapping it:
1. Opens a camera modal (same pattern as expiry scanner)
2. User takes photo of product/label
3. Sends to gemini_identify — returns name, brand, category + OpenFoodFacts matches
4. User can pick a specific OFF match (fills barcode + full details via lookup_barcode)
or tap 'Usa dati AI' to fill just name/brand/category from Gemini
5. All matching fields are auto-filled: name, brand, category, barcode, image, unit/qty
6. Button hidden when editing an existing product (not needed)
Spesa mode banner:
- Tracks each added product in _spesaSession[]
- Shows a rotating stat/phrase below the title: count, top category,
duplicates, fun milestone messages (primo prodotto, ottimo ritmo, spesa epica…)
- Banner gains two-line layout (title + stat)
Scan zoom:
- Small pill button 'x1'/'x2' overlaid top-right of the camera viewport
- On hardware-zoom capable devices (Android Chrome) uses track.applyConstraints zoom
- Falls back to CSS scale(2) on video element for all other browsers
- Zoom resets to x1 on stopScanner()
If a product was created with unit='g' (or ml/kg/l) and a default_quantity,
that value already IS the package size — no need to ask again.
Applied in both showAddForm() initial render and onAddUnitChange() toggle.
- 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