Commit Graph

139 Commits

Author SHA1 Message Date
dadaloop82 7144ec7386 feat(scale): g→ml density conversion for liquid products
- _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
2026-04-15 21:08:41 +00:00
dadaloop82 d26229800c feat(scale): auto-fill use-quantity live from scale reading
- _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
2026-04-15 21:06:12 +00:00
dadaloop82 6f5bc15734 fix: use relative API paths (leading / broke /dispensa/ subdir routing) 2026-04-15 21:00:34 +00:00
dadaloop82 55c5b34381 feat(scale): auto-discover gateway on local network
- 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
2026-04-15 20:56:54 +00:00
dadaloop82 099a6cc4e8 fix: HTTPS/WebSocket mixed-content — add PHP SSE relay for scale gateway
The browser (HTTPS) cannot connect to ws:// directly (mixed-content block).
Solution: PHP SSE relay bridges the gap server-side.

- api/scale_relay.php: GET ?url=ws://ip:port
  PHP opens WS connection to Android gateway, streams JSON frames as
  Server-Sent Events to the browser over existing HTTPS connection.
  Includes WS handshake, masked client frames, frame decoder, keep-alive.

- api/scale_ping.php: GET ?url=ws://ip:port
  One-shot connectivity test, returns {"ok":true} or {"ok":false,"error"}.

- app.js: WebSocket -> EventSource (SSE)
  _scaleWs -> _scaleEs, connects to /api/scale_relay.php
  testScaleConnection() -> fetch /api/scale_ping.php (no more direct ws://)
  readScaleWeight(): removed get_weight send (scale streams continuously)
2026-04-15 20:31:54 +00:00
dadaloop82 4d972b824e fix: vacuum state in move-after-use modal + show all recipe ingredients in cooking mode
- 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)
2026-04-15 11:05:23 +00:00
dadaloop82 0893742f05 feat: add smart scale BLE gateway integration
- 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
2026-04-14 15:59:40 +00:00
dadaloop82 c52a91e779 fix: restore app.js after sed truncation, update nav title to EverShelf
- 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
2026-04-13 10:14:40 +00:00
dadaloop82 20f734d54a rebrand: rename project from Dispensa Manager to EverShelf
- Update app name across all files (manifest, index.html, README, docs)
- Update contact email to evershelfproject@gmail.com
- Rename Docker service/container/volume to evershelf
- Rename localStorage keys: dispensa_* → evershelf_*
- Rename SQLite DB reference: dispensa.db → evershelf.db
- Update SSH remote to dadaloop82/EverShelf
- Update Apache conf file name to evershelf.conf
- Update CI workflow Docker image/container names
- Update cron job example path
- Add data/dispensa.db to .gitignore to prevent accidental commit
2026-04-13 10:09:33 +00:00
dadaloop82 da962581c0 feat: offline browser TTS engine with voice selector
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
2026-04-10 10:19:02 +00:00
dadaloop82 4b5979333e fix: use bring_password_set and gemini_key_set flags from server API
The server never exposes bring_password in plaintext (only bring_password_set).
Fix wizard to check the boolean flag instead of the empty string.
2026-04-10 06:57:46 +00:00
dadaloop82 82f147d8d5 fix: check server-side credentials before showing setup wizard steps
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.
2026-04-10 06:55:40 +00:00
dadaloop82 ef654b9dfc feat: smart setup wizard - only prompts for missing settings
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.
2026-04-10 06:53:03 +00:00
dadaloop82 d13f744aea feat: v1.1.0 - Docker, i18n, setup wizard, rate limiting, OpenAPI
New features:
- Docker support (Dockerfile + docker-compose.yml)
- GitHub Actions CI pipeline (PHP lint, JS lint, Docker build, i18n validation)
- Internationalization system with 3 languages (it, en, de) and 347 translation keys
- First-run setup wizard (4-step configuration)
- File-based API rate limiting (120/15/5 req/min tiers)
- OpenAPI 3.1.0 specification for all 43 API endpoints
- CONTRIBUTING.md with translation and development guide
- Screenshots directory placeholder

Modified:
- README.md: Docker badges, install instructions, translations section
- api/index.php: rate limiting middleware
- assets/js/app.js: i18n system, setup wizard, t() function
- assets/css/style.css: setup wizard styles
- index.html: data-i18n attributes, setup wizard overlay, language settings
- .gitignore: rate_limits exclusion
2026-04-10 06:03:11 +00:00
dadaloop82 e0956c6043 Prepare for public distribution v1.0.0
- Remove all personal data from source code (HA IP, JWT tokens)
- Move secrets to .env configuration (gitignored)
- Create .env.example template for new installations
- Add centralized env() helper, eliminate code duplication (~120 lines removed)
- Add input validation on inventory operations (quantity bounds, location whitelist)
- Remove sensitive credential exposure in API responses
- Remove database and runtime files from Git tracking
- Disable database push-to-GitHub backup (local-only backup now)
- Update .gitignore for distribution
- Add comprehensive README with installation guide
- Add CHANGELOG.md for version tracking
- Add MIT LICENSE
- Add author/license headers to all source files
- TTS defaults now empty (configured per-installation via .env)
2026-04-10 05:24:27 +00:00
dadaloop82 6531765921 Priorità verdura/frutta aperta in ricette + stime corrette
Stime opened (PHP+JS):
- Avocado -> 2d; fragole/banane/pesca/mango -> 2d
- Mela/pera/kiwi/ananas/melone/uva -> 3d
- Zucchina/melanzana/pomodoro/peperone -> 3d (era 7d)
- Broccolo/cavolfiore/sedano/finocchio -> 3d (era 7d)
- Cipolla/cipollotto/scalogno/porro -> 4d (era 7d)
- Carota -> 5d (era 7d); Aglio -> 10d
- Insalata/rucola/spinaci -> 2d (era 4d)

Recipe generator (index.php):
- SQL include i.opened_at
- getItemPriority: rileva opened_at su QUALSIASI unità (non solo conf)
- Elementi [APERTO] con scadenza <=5gg promossi in 'fortemente consigliati'
- Label '📦 PRODOTTI APERTI' aggiornata
- [APERTO] mostrato nel testo ingredienti del prompt AI
- Stesso fix nel contesto chat (opened_at + [APERTO] label)

DB migration: ricalcolate scadenze aperti con nuove stime
2026-04-09 12:56:10 +00:00
dadaloop82 d690ad826c Suggerisci confezione da usare per prima (scad. più vicina)
- 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)
2026-04-09 05:12:25 +00:00
dadaloop82 346f69426f Fix: 'Modifica scheda prodotto' duplicato
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.
2026-04-09 05:08:47 +00:00
dadaloop82 f86059cdcd Auto-refresh: 5min pagina corrente + 2min lista spesa bg
- 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)
2026-04-09 05:07:16 +00:00
dadaloop82 7f0d4d817b Auto-refresh dati ogni 10 min + visibilitychange
- 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
2026-04-09 05:03:47 +00:00
dadaloop82 48543ee8c4 Fix opened expiry: estimation rewrite + 0ml display + HTTPS info
- Restructure estimateOpenedExpiryDays (PHP+JS): check 'eternal' items FIRST
  before any location check, so they never get 5/30d regardless of shelf:
  - Salt/sugar/honey/vinegar/bicarbonato -> 9999d (never expires)
  - Spirits -> 730d, aroma/extracts/tea -> 730d, olio/coffee -> 365d
  - Pasta/rice/dry legumes (non-frigo) -> 365d; Polenta/flour -> 180d
  - Maionese/ketchup/senape -> 90d, soy sauce -> 90d anywhere
  - Dispensa fallback: 60d (was 30d); pantry salsa di pomodoro -> 5d
  - Fruit/veg in fridge: specific rules (banana 10d, citrus 14d, etc.)
  - burro 30d, panna 4d, mortadella/wurstel 5d (improved from old values)
- Fix dashboard (getStats): remove min() with stored expiry_date (was from old
  wrong estimation); use opened_at + new estimate directly
- Filter opened list: skip items with days_to_expiry > 365 (non-perishables)
- Fix useFromInventory (both paths): min(opened_estimate, sealed_expiry)
  so original sealed expiry is respected if it expires sooner
- DB migration: re-compute expiry_date for all 22 opened inventory rows
  (maionese: +90d, salt/sugar/aceto: +9999d, muesli/biscotti: +60d, etc.)
- Clear opened_at for 'inchusa' birra (user confirmed: not actually opened)
- Fix '4 conf + 0ml' display: only show remainder if remainderAmt >= 1
- Add ' Stabile' expiry badge for days > 365 (JS)
- Add dispensa-ca.crt to /data/ for browser import (HTTPS trust)
2026-04-08 14:29:44 +00:00
dadaloop82 19489a0265 Smart opened-product expiry: days countdown, edibility, correct sort
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
2026-04-08 12:30:36 +00:00
dadaloop82 dccda8ebc9 Fix: any-token product family grouping + auto timer reset on cache change
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
2026-04-07 15:26:35 +00:00
dadaloop82 dcc7e9de42 Fix smart shopping: skip depleted products with equivalent in-stock substitutes
PHP smartShopping():
- Add nameTokens() helper (mirrors JS _nameTokens)
- Build stockByFirstToken map before product loop
- Skip depleted (qty=0) products whose first token has stock elsewhere
  → 'Aglio rosso' depleted but 'Aglio' qty=3 → skip
  → 'Latte Parzialmente Scremato' depleted but 'Latte di Montagna' 4.8 conf → skip
  → 'Muesli Frutta Secca' depleted but 'Muesli multifrutta' 930g → skip
- Result: 13→9 items, no more false critical flagging for covered products

JS cleanupObsoleteBringItems():
- Rewrite with stockByFirstToken approach (aggregate by first token, not product_id)
- urgentMatch logic: if smart item is completely depleted (qty=0) but equivalent
  stock exists via first token → still remove from Bring (need is covered)
- Only keep Bring item if: smart flags it with current_qty>0 (genuinely running low)

Also: removed Milch/Knoblauch/Fruechte/Passata from Bring directly (immediate fix)
2026-04-07 15:20:33 +00:00
dadaloop82 0bca79b8a2 Fix 3 bugs: banana use blocked, cleanup never ran, stale Bring items
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
2026-04-07 15:08:03 +00:00
dadaloop82 9b51bb606d Fix smart shopping false positives (prodotti appena comprati/sufficienti)
PHP smartShopping():
- Absolute minimum fallback: requires isRegular + buyCount>=2 + pctLeft<80
  (before: ANY product with buyCount>=1 → triggered for newly bought items)
- Add justRestocked guard: skip item if bought within 3 days AND pctLeft>=50%
  and not expiring (prevents items bought yesterday showing as urgent)
- Add daysSinceLastBuy calculation

JS isLowStock():
- pz threshold: <=1 (was <=2) — 2 pezzi rimasti non è già urgente
2026-04-07 15:02:15 +00:00
dadaloop82 72535ce41c UX: fraction buttons for pz unit in use form + fix qty display
- _pzFractionLabel(): formats 2.5 → '2½', 2.25 → '2¼', etc.
- formatQuantity/formatQuantityParts: use fraction label for pz (was rounding to int)
- loadUseInventoryInfo normal mode: show ¼/½/¾/1 intero quick-select buttons for pz
- setPzFraction(): sets quantity input + syncs active button highlight
- adjustUseQty pz: step = 0.5 (half-piece) instead of 1; syncs frac button on ±
- CSS: .pz-fraction-btns, .fraction-btn-row, .frac-btn, .frac-btn.active
2026-04-07 13:03:32 +00:00
dadaloop82 dc36ce2ae4 fix: MODIFICA button now edits inventory instance (expiry/location/qty/vacuum)
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.
2026-04-07 12:52:31 +00:00
dadaloop82 4e576559a9 feat: barcode scan button + reminder in manual product form
- 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)
2026-04-07 12:17:18 +00:00
dadaloop82 b7ed9899fa feat+fix: Bring removal, multi-expiry batches, FIFO in cooking steps
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
2026-04-07 12:10:14 +00:00
dadaloop82 5be62cfbfd fix: low stock detection for rarely-used items
- 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.
2026-04-06 10:53:15 +00:00
dadaloop82 b47dcb4fac fix: TTS only on Rileggi btn; use-all deducts from all locations
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
2026-04-06 10:23:03 +00:00
dadaloop82 4e8b586201 feat: AI photo identification from product form
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)
2026-04-06 09:23:41 +00:00
dadaloop82 a6bc05cd2d feat: spesa mode stats banner + scan zoom x1/x2 toggle
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()
2026-04-06 09:16:50 +00:00
dadaloop82 7782eb1519 fix: pre-fill conf size from product's weight/volume unit when switching to 'confezioni'
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.
2026-04-06 09:10:29 +00:00
dadaloop82 50da545c72 feat: predict expiry date from product history when adding items
- 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
2026-04-06 09:09:04 +00:00
dadaloop82 568cc1e6fa fix: don't re-add items to Bring after user removes them (purchased blocklist, 4h TTL) 2026-04-06 08:53:14 +00:00
dadaloop82 57677fa0d0 fix: keep previous settings (meal, persons, options) on regenerate 2026-04-04 15:35:07 +00:00
dadaloop82 e233dcef6d fix: remove duplicate const meal declaration in regenerateRecipe 2026-04-04 15:31:31 +00:00
dadaloop82 da5552e992 fix: hide meal-plan banner on chip uncheck; fix recipe variety (variation counter, temp scaling, client-side title tracking) 2026-04-04 15:29:07 +00:00
dadaloop82 bd6f92f2f3 fix: route TTS through PHP proxy to bypass mixed-content/CORS 2026-04-04 14:44:11 +00:00
dadaloop82 475d482184 feat: TTS generic API builder, remove HA refs, pre-fill credentials 2026-04-04 14:40:48 +00:00
dadaloop82 7bc1c87d5c feat: TTS via Home Assistant API, settings panel, remove browser speechSynthesis 2026-04-04 14:37:00 +00:00
dadaloop82 63db7cc114 feat: bring urgency sync, background auto-sync, recipe mealplan chip, screensaver fix 2026-04-04 14:32:25 +00:00
dadaloop82 e18fb5839a Smart shopping: cron ogni 5min pre-calcola cache server-side, API serve da cache (risposta istantanea) 2026-04-01 05:52:17 +00:00
dadaloop82 fb7bb4d675 Modalità cucina: timer multipli persistenti con etichetta, riprendi dal passo salvato, progress dots, pulsante Ricomincia; priorità ricette basata su scadenze con ingredienti obbligatori 2026-03-31 15:55:35 +00:00
dadaloop82 bcddba46d4 Remove kg/l units everywhere — only g (grammi) and ml (millilitri)
- 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)
2026-03-30 14:13:11 +00:00
dadaloop82 c4938457ac Fix min quantity for kg/l units in use forms
- 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
2026-03-30 13:45:02 +00:00
dadaloop82 c63faf56e4 Conservative Bring! cleanup + operations log
- 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
2026-03-30 13:36:51 +00:00
dadaloop82 4b3e5f2ce4 Cleanup obsolete Bring! items after smart shopping algorithm fix
- 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
2026-03-29 19:54:05 +00:00