- 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
- 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
- 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
- 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.
- 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)
- Green-themed banner between settings panels and Save button
- Direct download link to kiosk-latest release APK
- Auto-hidden when running inside Android WebView (kiosk mode)
- i18n: translations added for it/en/de
- Fix progress-bar restarting continuously when weight is stable:
add _cancelScaleTimersOnly() that stops timers/animations without
_cancelScaleTimersOnly() so the same value resumes counting when
stability returns instead of always restarting the 5-s wait.
Add 'else if' branch in _scaleAutoFillUse / _scaleAutoFillRecipeUse
to restart stability wait after brief instability for the same value.
- Show red blinking warning in scale-live-box when weight < 10 g:
adds scale-low-weight CSS class with pulsing border/shadow animation,
the label shows '< 10 g · inserisci manualmente' instead of the
stability progress bar. No auto-confirm fires below 10 g.
- Gateway Android app: scale auto-reconnect now retries indefinitely.
isAutoReconnecting flag keeps the scan→wait→scan cycle running until
the scale is found again; onScanStopped schedules a new scan after
10 s whenever autoReconnect is active and scale is still offline.
Two root causes:
1. _useNormalUnit was stale ('pz') for conf products because it's only
updated in normal mode — fix: resolve effective unit from
_useConfMode.packageUnit when in sub mode
2. Food scales in liquid mode send unit='ml' directly — was falling
through to raw value, skipping density; fix: detect scaleAlreadyMl
flag to use ml directly for ml target, or apply density for g target
Also: add scale pre-fill call after switchUseUnit('sub') in conf mode
- _scaleDensityForProduct(): returns g/ml density based on product name/category
olio oliva 0.91, girasole 0.92, spirits 0.94, aceto/panna 1.01, latte 1.03,
yogurt 1.05, succo 1.04, miele/sciroppo 1.40, default 1.00 (water)
- _scaleAutoFillUse(): normalises all scale units to grams first, then converts
to target unit; when unit=ml applies density; never touches pz/conf
- _scaleAutoFillUse(): converts and fills use-quantity on stable readings
- _scaleLatestWeight pre-fills when use page opens (after unit is known)
- oninput on use-quantity pauses auto-fill when user types manually
- showUseForm() resets paused flag so next opening resumes auto-fill
- Small 'live' hint text shown while auto-filling, hidden when overridden
- 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
- Recover app.js from pre-rebrand commit and re-apply all substitutions safely
- Fix all localStorage keys: dispensa_* -> evershelf_* (settings, setup, lang)
- Fix TTS test strings: 'Dispensa Manager' -> 'EverShelf'
- Set nav.title to EverShelf in it/en/de translations and HTML fallback
- Fix JS syntax error (Unexpected end of input) that broke the site
- CI JavaScript Lint should now pass
Add Web Speech API as alternative TTS engine (fully offline, no config needed).
- Engine selector in settings: 'browser' (offline) or 'server' (HTTP endpoint)
- Voice picker populated from speechSynthesis.getVoices(), Italian voices first
- Auto-selects Paola voice on macOS/iOS if available
- Rate and pitch sliders (0.5x-2x, 0-2)
- testTTS() and speakCookingStep() branch on selected engine
- Existing users with tts_url keep 'server' as default engine
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