Bring! and Gemini keys stored in .env are now fetched from the server
before deciding which wizard steps to show. This prevents the wizard
from prompting for credentials that are already configured server-side.
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
- 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)
- Normal mode (non-conf) now sets min=0.01 for kg/l, min=1 for g/ml
- +/- buttons use unit-aware steps: 0.01 for small kg/l values, 0.1 for
values <1, 0.5 for values >=1 (instead of fixed 0.5)
- Same fix applied to recipe use form
- Allows inputting e.g. 0.07kg (70g) when product is tracked in kg
- cleanupObsoleteBringItems() now much more conservative:
* Only removes items matching a known DB product (preserves manual additions)
* Only removes if the product has current_qty > 0 (has stock)
* AND item is no longer flagged by smart shopping
- Added logOperation() — stores all Bring! operations in localStorage '_opLog'
(bring_auto_add, bring_cleanup, bring_found, bring_manual_remove)
Capped at 200 entries, each with timestamp + action + details
- All Bring! add/remove paths now log their operations
- cleanupObsoleteBringItems(): one-time per session, removes items from Bring!
that the updated smart shopping algorithm no longer considers relevant
- Cross-references shoppingItems vs smartShoppingItems using exact + token match
- Shows toast with count of removed items
- Called alongside autoAddCriticalItems after loading smart shopping data
- Rileva automaticamente durate nel testo dello step (minuti, ore, secondi,
mezz'ora, un quarto d'ora, qualche minuto, un paio di minuti, ecc.)
- Mostra countdown grande con Avvia/Pausa/Reset
- Ultimi 30 secondi in arancione, scaduto in rosso pulsante
- Allo scadere: vibrazione + TTS 'Tempo scaduto!'
- Timer continua a contare in overtime (+00:XX) dopo lo zero
- Timer si resetta automaticamente cambiando step o chiudendo
- startCookingMode: attende voiceschanged prima di parlare il primo step
cosi la voce Google (donna) viene usata subito, non il fallback robot
- _cookingTTS parte sempre true (attivato), icona 🔊
- Bottone Rileggi visibile di default senza display:none
- Cache buster aggiornato a v=20260329c
- Aggiunta helper _nameTokens() + _findSimilarItem() per confronto
per token tra nomi prodotto (gestisce marche diverse / varianti)
1. Se un prodotto simile e' gia' nella lista Bring! (shoppingItems)
→ mostra solo un toast, non apre il modale
2. Se la spesa intelligente prevede gia' il prodotto (smartShoppingItems)
→ mostra nota contestuale nel modale con urgenza
- renderShoppingItems: raggruppamento per reparto (sezioni), ordinamento
per urgenza+frequenza, sfondo con gradiente colore urgenza
- renderSmartShopping: stesso raggruppamento per reparto in tab previsione
- Modalità Cucina: overlay fullscreen nero, step per step con navigazione,
TTS italiano via Web Speech API, pulsante 'Usa' ingredienti per step
- CSS: modal z-index 600 in cooking-mode-active per sovrapposizione corretta
- 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