Commit Graph

114 Commits

Author SHA1 Message Date
dadaloop82 4196130835 feat: AI suggestions, smart shopping qty, shelf life fixes, UX polish
- bringSuggestItems(): Gemini AI for seasonal/complementary suggestions (6h cache)
- renderSuggestions(): AI badge (🤖 AI) for AI-sourced items + CSS .priority-ai
- smartShopping(): suggested_qty/unit/approx with package-aware tiers
- autoSyncUrgencySpecs(): sync suggested quantities to Bring! spec field
- estimateOpenedExpiryDays(): dairy-outside-fridge rules (panna 3d, yogurt 2d, latte 1d)
- AI shelf-life upper bound tightened to max(rule×4, 30) days
- Opened section: fix 0g display (remainderAmt >= 0.5 threshold, pkgSize guard)
- guessCategoryFromName(): expanded with 50+ new patterns (uova, herbs, vegetables...)
- Suggestions panel: excludes already-added Bring! items
- Shopping list: no re-render while suggestions panel is open
- Translations: remove duplicate 🍳 from dashboard.quick_recipe (all 3 langs)
- Scale icon: always white via filter:brightness(0)invert(1)
- opened_shelf_cache.json: remove 3 bad dairy entries (60d outside fridge)
2026-05-07 06:19:07 +00:00
dadaloop82 ffb0341eb6 feat: screensaver configurable timeout + fix gitignore (exclude kiosk build artifacts) 2026-05-06 15:00:04 +00:00
dadaloop82 521d8f8e47 fix: screensaver init timing + gemini key not wiped on settings save
- app.js: move initInactivityWatcher() inside syncSettingsFromDB().then()
  so it reads screensaver_enabled after server sync, not stale localStorage
- app.js: skip gemini_key/bring_password in save_settings POST when empty
  to avoid overwriting server .env with blank values
- api/index.php: add screensaver_enabled to getServerSettings() + saveSettings()
2026-05-06 05:14:10 +00:00
dadaloop82 4f6592b749 fix: install_failure type mismatch — issue never reached GitHub
SetupActivity was sending type 'install-failure' (hyphen) but the PHP
version-guard bypass list only checked for 'install_failure' (underscore).
Result: if the kiosk was not on the latest released version the error was
silently discarded and no GitHub issue was created.

Fix:
- SetupActivity: change type to 'install_failure' (underscore, consistent
  with KioskActivity which already used the correct name)
- api/index.php: add 'install-failure' (hyphen) to the bypass list as
  defensive fallback so old APK builds already in the field are covered too
2026-05-04 17:45:46 +00:00
dadaloop82 04cba79519 chore: update GitHub issue reporter token 2026-05-04 16:06:12 +00:00
dadaloop82 bf27469228 security: fix 3 critical vulnerabilities
1. Remove raw API key from get_settings response
   - getServerSettings() no longer returns gemini_key in plain text
   - Only gemini_key_set (boolean) and settings_token_set (boolean)
   - JS updated to only check gemini_key_set (removes stale gemini_key fallback)

2. Protect save_settings with SETTINGS_TOKEN
   - If SETTINGS_TOKEN is set in .env, all save_settings calls must
     include matching X-Settings-Token header (uses hash_equals)
   - Empty token = no protection (backwards-compatible default)
   - Settings UI (Security tab) has a token input field
   - Wrong/missing token returns HTTP 403 with error 'unauthorized'
   - JS shows '🔒 Token non valido o mancante' on 403

3. DEMO_MODE native blocking in PHP
   - DEMO_MODE=false added to .env (default off)
   - When DEMO_MODE=true, all write actions return HTTP 403 before routing
   - Blocked: save_settings, product_save/delete/merge, inventory_add/use/update/remove,
     dismiss_anomaly, bring_add/remove/sync
   - demo_mode flag exposed via get_settings so JS can adapt UI
2026-05-04 06:20:23 +00:00
dadaloop82 529c09fda3 feat(ai): 3 new AI features — product storage hint, shopping tips, anomaly explain
Feature 1: AI product storage/shelf-life hint
- New API: gemini_product_hint → {location, expiry_days, reason}
- After opening the add form, Gemini suggests optimal storage and expiry
- Shown inline next to expiry estimate as a subtle AI badge with tooltip
- Also updates location buttons if AI suggests a different location
- Cached permanently in food_facts_cache.json (per name+lang)

Feature 2: AI-enriched shopping suggestions
- New API: gemini_shopping_enrich → adds tip field to each suggestion
- After bring_suggest renders, Gemini adds practical buying/storing tips
- Tips shown inline under each suggestion item in indigo italic text
- Cached per item list + lang in food_facts_cache.json

Feature 3: AI anomaly explanation
- New API: gemini_anomaly_explain → plain-language explanation
- '🤖 Spiega' button added to anomaly banners (when Gemini available)
- Explains in 2-3 conversational sentences why the discrepancy likely happened
- Replaces technical banner detail text with friendly explanation
- No caching (anomaly context is always specific)
2026-05-04 06:01:44 +00:00
dadaloop82 c6e3d13e8c fix(api): bringAddItems() missing $input/$items decode — always returned 0
bringAddItems() used $input and $items without ever decoding the request
body. $items was undefined (null) so the foreach never ran, every call
returned added=0 skipped=0 regardless of what was sent.

Added:
  $input = json_decode(file_get_contents('php://input'), true) ?? [];
  $items = $input['items'] ?? [];
Also added the missing $auth guard (consistent with all other Bring functions).
2026-05-04 05:21:07 +00:00
dadaloop82 15e1dfbd69 fix(kiosk): STATUS_FAILURE=1 (wrong package) + issue version-guard bypass
Bug 1 — Root cause of PackageInstaller STATUS_FAILURE=1:
  The dest file is always named 'evershelf-update.apk'. installApk()
  was trying to detect 'gateway' in the filename — always false.
  So setAppPackageName() was always passed 'it.dadaloop.evershelf.kiosk'
  even when installing the gateway APK (package scalegate).
  PackageInstaller rejects the mismatch with STATUS_FAILURE=1.

  Fix: save apkUrl into pendingApkDownloadUrl at the TOP of
  triggerApkDownload() (not only in the permission branch), then derive
  targetPkg from the URL (which does contain 'gateway'/'scale') instead
  of the filename.

Bug 2 — Install errors not reaching GitHub Issues:
  PHP reportError() has a version guard: if the client version is not
  the latest release, it silently skips GitHub issue creation.
  A device that is FAILING TO INSTALL an update is by definition on an
  old version, so every install error was silently dropped.

  Fix: bypass the version guard for types install_download_failed,
  install_failure, install_packager_exception.
2026-05-03 19:29:04 +00:00
dadaloop82 58e69625bd fix: preloader + update notification robustness
- Add full-screen CSS preloader to webapp (fades out when _initApp completes)
- Defer _checkWebappUpdate() to 6s after app init so it does not compete
  with startup API calls (fixes perceived slowness on first load)
- Switch update-check throttle from sessionStorage to localStorage (6h TTL);
  use release published_at instead of version string for comparison, so the
  banner correctly appears when a new release is published regardless of whether
  the tag is a semver or the rolling "latest" tag
- PHP _isLatestVersion(): return true (do not suppress error reports) when
  tag_name is non-semver (e.g. "latest") — was incorrectly blocking ALL reports
- Kiosk checkForUpdates(): show banner only when the release asset actually
  contains an APK for the component; handle non-semver tag by treating it
  as always-update (prevents silent no-op with rolling "latest" tag)
- Scale gateway checkForUpdates(): same non-semver fix; apkUrl now defaults
  to empty and bails out if no matching APK found in assets (prevents 404 install)
2026-05-03 17:46:42 +00:00
dadaloop82 076cf13ed8 feat: version-aware error reporting, XOR token, update banners
in both PHP (api/index.php) and Scale Gateway (ErrorReporter.kt)
- Add _isLatestVersion() / _latestReleaseTag() / _appVersion() helpers in PHP;
  skip GitHub issue creation if caller is not on the latest released version
- Add checkUpdate() PHP endpoint (GET api/?action=check_update, no auth required)
- Webapp (app.js): fetch check_update on load, show dismissible amber top-banner
  when a newer GitHub release is available; auto-dismiss after 20 s
- Kiosk (KioskActivity.kt + activity_kiosk.xml): replace old JS bottom-banner with
  native Android top-banner; real APK download via DownloadManager + PackageInstaller
- Scale Gateway (MainActivity.kt + activity_main.xml): same native top-banner
  with checkForUpdates() / showNativeUpdateBanner() / triggerApkDownload() / installApk()
2026-05-03 17:24:26 +00:00
dadaloop82 ea40c8e02b feat: centralized error reporting → GitHub Issues
- PHP (api/index.php): hardcode GH_ISSUE_TOKEN/GH_REPO constants at top of
  file (before exception handler runs); fix $fp_ variable interpolation bug;
  global set_exception_handler + register_shutdown_function; reportError()
  endpoint (POST ?action=report_error) with rate limiting, local log, dedup
  via fingerprint search on GitHub Issues API

- Kiosk (ErrorReporter.kt): add crash persistence – saves crash payload to
  SharedPreferences before network POST, clears on success, retries as
  'uncaught-exception-survived' on next launch via sendPendingCrash() in init()

- Scale Gateway: new ErrorReporter.kt – calls GitHub Issues API directly
  (no relay needed, token hardcoded, scoped Issues R+W only); crash
  persistence via SharedPreferences; MainActivity.kt hooked at onCreate,
  startGatewayServer catch, onError (BLE errors)

Tested end-to-end: issues #3-#6 created and closed during QA.
2026-05-03 17:11:11 +00:00
dadaloop82 f2e151d89b feat: centralized error reporting → auto GitHub Issues
PHP (api/index.php):
- reportError() endpoint (POST ?action=report_error): accepts source/type/message/stack/context/ua/version
- _createOrCommentGithubIssue(): creates new issue OR adds comment on existing one (dedup by sha1 fingerprint via GitHub search API)
- _appendErrorLog(): local data/error_reports.log fallback (500 KB rotation)
- _phpErrorReport(): called by set_exception_handler + register_shutdown_function → catches all PHP fatals and uncaught exceptions
- _githubRequest(): minimal curl-based GitHub REST v3 helper
- Rate limit bucket: error_report (20 req/min)
- Labels auto-created: auto-report, php-crash, js-error, kiosk-error, scale-error

JS (assets/js/app.js):
- reportError(payload): single POST to report_error, session-level dedup via _reportedFingerprints Set
- window.onerror: reports uncaught-error with message+stack+location context
- window.unhandledrejection: reports unhandled-promise with reason+stack
- api(): reports api-server-error on HTTP 5xx responses

Android Kiosk:
- ErrorReporter.kt: singleton with init(context, serverUrl), report(Throwable), reportMessage(type, message)
  - Thread.setDefaultUncaughtExceptionHandler → catches ALL unhandled JVM crashes
  - Async executor (single thread), per-session fingerprint dedup, synchronous fallback for crash handler
  - doPost(): HttpURLConnection POST to /api/?action=report_error with device/version info
- KioskActivity: ErrorReporter.init() in onCreate + finishWizard()
  - onReceivedError: reports webview-load-error with URL + error code
  - onConsoleMessage: reports webview-js-error for ERROR level console messages

Config: GITHUB_ISSUE_TOKEN + GITHUB_REPO added to .env.example
2026-05-03 15:36:03 +00:00
dadaloop82 a6c2fb93cf feat: offline OCR (Tesseract) + embedding category classifier (@xenova/transformers)
Tesseract OCR (PHP, server-side):
- Dockerfile: adds tesseract-ocr + tesseract-ocr-ita + libgd-dev (gd extension)
- api/index.php: new tesseractReadExpiry() — decodes base64 image, pre-processes with GD (2× upscale, greyscale, auto-contrast, sharpen), runs tesseract CLI with ita+eng PSM-6, extracts date with multi-pattern regex (DD/MM/YYYY, MM/YYYY, ISO, named-month), returns YYYY-MM-DD + confidence
- geminiReadExpiry() now: (1) tries Tesseract first; (2) falls back to Gemini Vision if OCR returns null or no date found; (3) passes source ('ocr'|'gemini') in response

@xenova/transformers embedding classifier (browser-side):
- index.html: ES-module bootstrap that lazy-loads 'Xenova/all-MiniLM-L6-v2' quantized (~23 MB, cached in browser) via window._getCategoryPipeline(); pre-warms on first scan page visit
- assets/js/app.js: classifyCategoryByEmbedding(name) — embeds product name + 16 category anchor descriptions, cosine similarity, threshold 0.30; results cached in _embeddingCache Map
- autoDetectCategory(): after keyword map misses, fires classifyCategoryByEmbedding async and updates select when resolved (respects manuallySet flag)
- createQuickProduct(): if regex returned 'altro', silently patches category with embedding result via a background api call
2026-05-03 13:17:14 +00:00
dadaloop82 8d02e76501 Remove interval% from annual waste info; fix conf whole-qty using package expiry not opened shelf-life 2026-04-29 17:05:38 +00:00
dadaloop82 e71ef3aba3 Dashboard: move waste-chart above expiring; fix opened-items conf split, expiry cache, AI validation, MAX_SHOWN 20; remove DupliClick from README 2026-04-29 17:02:10 +00:00
dadaloop82 3c9fe7dfea Remove all Dupliclick/Spesa integration; merge annual waste info into status line 2026-04-29 16:52:36 +00:00
dadaloop82 da46fec174 Fix anomaly banner when expected_qty is negative (untracked initial stock) 2026-04-29 15:26:59 +00:00
dadaloop82 22266cb620 Fix sealed/opened expiry; AI shelf-life cache; redesign waste UI 2026-04-29 06:42:21 +00:00
dadaloop82 e002955173 Anti-waste: daily food-facts API, 3-badge rotating row with fade 2026-04-29 06:28:46 +00:00
dadaloop82 7c4dd99289 Anti-waste: themed border, rich info badges, fix latte di montagna shelf-life, exclude opened from expiring_soon 2026-04-29 06:19:35 +00:00
dadaloop82 ee2c280167 Redesign anti-waste section: report card with grade, comparison vs national avg, savings badges and trend chart
- Replace simple bar chart with full Anti-Waste Report Card
- Grade system (A+ to D) based on user's waste rate
- Dual comparison bars: user waste rate vs national average (IT/DE/US)
- Estimated monthly savings in money, meals saved, CO2 avoided
- 3-month trend mini chart with colour-coded bars
- Backend: getStats() now returns 3×30d buckets (used_30d, used_prev_30d, used_prev_60d, etc.)
- Real-world benchmarks: IT 22%/5.4kg/mo (REDUCE), DE 20%/6.5kg/mo (Eurostat), US 30%/9.2kg/mo (USDA)
- All labels fully i18n: 18 new antiwaste.* keys in it/en/de translation files
- Section is fully JS-rendered; HTML now just an empty container
2026-04-29 05:54:17 +00:00
dadaloop82 2c06be33d4 Improve use-flow UX and suppress redundant finished alerts 2026-04-29 05:38:21 +00:00
dadaloop82 8558db1925 Complete i18n pass for recipes and meal plan labels 2026-04-28 17:28:54 +00:00
dadaloop82 c3b19a6c48 feat: expired banner for opened products, AI model fallback, TTS cooking improvements
- Banner: detect expired opened-products via effective shelf-life (opened_at +
  estimateOpenedExpiryDays), not just raw expiry_date — fixes Fagioli/Panna case
- Banner: expired items show safety tip inline; danger-level items (fridge dairy,
  meat, fish) get red banner + 'L'ho buttato' as primary button, 'Usa comunque'
  demoted to grey; safety-ok/warning items keep original button order
- Banner: anomaly dismiss button now shows current inventory qty ('La quantità è
  giusta (2 pz)') so the action is unambiguous
- AI: add callGeminiWithFallback() helper — tries gemini-2.5-flash first (separate
  quota), falls back to gemini-2.0-flash; applied to all endpoints (expiry, chat,
  identify, recipe non-streaming, shopping name classifier)
- AI: show friendly 'Quota AI esaurita' message instead of raw Gemini error string
- Cooking TTS: fix auto-speak broken since 'auto-speak removed' comment — each step
  is now read automatically on navigate and on first step when entering cooking mode
- Cooking TTS: remove incorrect s.tts_enabled gate — _cookingTTS toggle is the only
  gate; browser Web Speech API used by default without requiring Settings config
- Cooking TTS: timer fires '10 secondi rimanenti' warning at T-10s
- Cooking TTS: announce recipe completion ('Buon appetito!') on last step confirm
- i18n: add timer_warning_tts, recipe_done_tts, error.ai_quota keys (IT/EN/DE)
- CSS: add banner-expired-danger, banner-safety-* styles for unsafe expired items
2026-04-28 12:46:00 +00:00
dadaloop82 608afb086d fix: bringMigrateNamesInternal — use PUT/remove and German catalog keys
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.
2026-04-27 18:14:27 +00:00
dadaloop82 cb75558581 fix: auto-migrate Bring! names to generic on every list load (throttled 10min)
- 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!
2026-04-27 17:33:49 +00:00
dadaloop82 8258591e44 feat: migrate existing Bring! items to generic shopping names
- 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
2026-04-27 17:29:55 +00:00
dadaloop82 28a8c938bd fix: prevent scale double-deduction (duplicate inventory_use)
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.
2026-04-27 17:01:11 +00:00
dadaloop82 a5a6e80b31 fix: use product_shopping_name in all Bring! add paths from low-stock flow
- 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
2026-04-27 13:45:10 +00:00
dadaloop82 fd5ff00d82 fix: comprehensive shopping name audit + README update
Shopping name system hardening (api/index.php):
- phraseMap: added sugar subtypes (zucchero a velo), passato di verdure/patate
  → Verdure, aroma phrases → Ingredienti Spezie, farina 00 → Farina,
  explicit brodo subtypes; added ordering comment on 'farina integrale'
- keywordMap: added 'lievito' → Lievito, 'aroma' → Ingredienti Spezie
  single-token fallbacks

DB migration (sqlite3 direct):
- farina integrale / Farina integrale di grano tenero → Farina integrale (3 rows)
- Prodotto sconosciuto (Belbake, it:farina-integrale) → Farina integrale
- Zucchero di canna → Zucchero di canna
- Passato di patate e carote → Verdure (it's a blended veg purée)
- Aroma mandorla per dolci → Ingredienti Spezie (consistent with other aromas)
Total: 11 products re-classified

README.md:
- Shopping List: added generic shopping name feature + auto-add on depletion
- Cooking Mode: updated TTS description (browser / native Android / REST)
- Kiosk: added native TTS bridge bullet
- Roadmap: checked off 3 new completed items
2026-04-27 12:17:11 +00:00
dadaloop82 1a73ed91dd fix: compound shopping names + auto-Bring on depletion + panna da cucina
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.
2026-04-27 12:04:48 +00:00
dadaloop82 8b5985dc80 feat: improve computeShoppingName — expanded keyword map + Gemini AI fallback
- 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
2026-04-27 11:40:14 +00:00
dadaloop82 430f9e7854 feat: generic shopping names — group products by shopping_name
- 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
2026-04-27 08:16:44 +00:00
dadaloop82 76c4344720 feat: show last 3 barcode digits in finished-product banner title 2026-04-27 05:50:22 +00:00
dadaloop82 61e7d7d4bf fix: finished banner only fires when transaction balance is suspicious
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.
2026-04-27 05:47:11 +00:00
dadaloop82 5df0be1661 feat: keep qty=0 instead of auto-delete, ask user to confirm via banner
- 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
2026-04-27 05:41:38 +00:00
dadaloop82 4a780f2743 feat: smarter alert banners — expired only, explanatory messages
- 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
2026-04-26 16:57:09 +00:00
dadaloop82 546d4afd59 feat: SSE streaming recipe generation with live agent feedback
- 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
2026-04-23 15:16:50 +00:00
dadaloop82 db033844d4 Gemini: centralizza chiamate API in callGemini() con backoff intelligente
- 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)
2026-04-22 11:38:47 +00:00
dadaloop82 f4a62ef496 feat: anomaly detection banner - notifica incongruenze inventario/transazioni
- 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!
2026-04-21 12:34:54 +00:00
dadaloop82 234cae14bc perf: remove Gemini from bringSuggest and product selection - pure offline logic
- bringSuggestItems: now uses smart_shopping cache (already computed offline)
  instead of calling Gemini with full inventory prompt
- aiSelectBestProduct: replaced Gemini call with token-scoring algorithm;
  scores by token overlap, first-anchor bonus, spec-variant matching,
  category mismatch penalty — 0ms vs ~1s per product search
- Only truly necessary AI calls remain: photo expiry, photo identify,
  chat assistant, recipe generation
2026-04-21 12:12:04 +00:00
dadaloop82 03142e2f7f fix: retry Gemini 429 with backoff, add recipe rate limit bucket (5/min) 2026-04-21 12:03:16 +00:00
dadaloop82 5bbedc8a3b fix: aggiorna urgenza lista spesa live e più frequentemente
- 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
2026-04-21 05:32:06 +00:00
dadaloop82 4db8882dbd feat: ottimizza prompt Gemini ricette (riduzione ~60% token) e migliora stabilità bilancia
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
2026-04-20 14:43:05 +00:00
dadaloop82 c115f83879 fix(bring): avoid ambiguous fallback match on generic words like 'dolce'
- 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
2026-04-19 09:07:44 +00:00
dadaloop82 1021f04735 fix: smarter proactive shopping list urgency
- 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.
2026-04-19 06:06:18 +00:00
dadaloop82 07bdfe6b87 fix: kiosk overlay, preferred use-location, scale reconnect, Bring! translation, smart cache invalidation
- 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
2026-04-18 18:50:15 +00:00
dadaloop82 9e2a24def4 feat: v1.3.0 — banner notifications, quick-access, swipe navigation, bug fixes
Added:
- Expired/expiring product banner alerts with use, throw, edit, dismiss actions
- Priority-sorted notifications (expired > expiring > suspicious qty > predictions)
- Touch swipe navigation for banner with dot indicators and arrow buttons
- Quick-access buttons on inventory (4 recent + 8 popular products)
- Auto-refresh banner every 5 min on dashboard
- Edit expiry dates directly from expired/expiring notifications

Fixed:
- Ignore negative BLE scale readings
- Banner re-appearing after edit (confirmation now persisted)
- False consumption predictions when inventory was manually edited
- Kiosk overlay no longer blocks web app header
2026-04-18 05:37:03 +00:00
dadaloop82 3e25fcd5df feat: banner alerts, consumption predictions, scale improvements, kiosk app
- Banner notification system: suspicious quantities + consumption prediction alerts
- Consumption predictions API: tracks 90-day usage patterns, flags >30% deviations
- Scale stability timeout: 5s → 10s, auto-confirm remains 5s
- Scale integration in edit form: weigh button with inline live display
- Banner edit/weigh actions open edit form directly with scale activation
- Cooking mode: Italian aliases + stem-prefix matching for ingredients
- Recipe regeneration: tracks rejected ingredients for diversity
- Settings migration: localStorage → .env server-side storage
- Expiry priority: mandatory ≤3 days, recommended ≤7 days in recipes
- Scale bug fixes: clear stale weight, double-submit guard, cap deduction
- Android kiosk app (evershelf-kiosk): WebView + embedded BLE scale gateway
- Version bump to 1.4.0
2026-04-16 14:46:30 +00:00