Commit Graph

415 Commits

Author SHA1 Message Date
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 a85390b498 feat(ai): guard all Gemini features when API key is not configured
Added _geminiAvailable global flag (false by default):
- Set in _initApp() from serverSettings.gemini_key_set after app loads
- Updated in syncSettingsFromDB() so it stays current if key is added later

Added _requireGemini() helper:
- Returns true if Gemini key is configured → proceed normally
- Returns false + shows a warning toast if key is missing → abort

Added _updateGeminiButtonState():
- Adds .header-btn-no-ai CSS class to Gemini button when key is missing:
  greyed out, slight grayscale filter, amber dot badge in corner
- Updates button tooltip to explain what to do
- Removes class/restores normal appearance when key is present

All 6 AI entry points now call _requireGemini() as first line:
  captureForAI()          — AI product identification from scan page
  captureForAIFormFill()  — AI product fill in manual add form
  scanExpiryWithAI()      — AI expiry date reader
  openRecipeDialog()      — recipe generation dialog
  generateRecipe()        — recipe generation (direct call path)
  quickRecipeSuggestion() — quick expiring-products recipe (→ chat)
  showPage('chat')        — Gemini chat page

Previously: user would click the button, camera would open, API call
would fail, and only THEN see an error message deep in the flow.
Now: blocked immediately at the entry point with a clear toast.
2026-05-04 05:50:30 +00:00
dadaloop82 d635635577 fix(ui): update notification inline in header title area, no full-page banner
Instead of a fixed banner that covers the top of the page, the update
notification now replaces only the header title area (the centered title):
- .header-title content is swapped in-place with an animated pill:
    ⬆️ v1.x.x  [Aggiorna]  ✕
- Pulsing animation (header-update-pulse) draws attention without being
  intrusive; camera and Gemini buttons stay exactly where they are
- [Aggiorna] button does window.location.reload()
- [✕] dismisses: for a release update stores publishedAt so it won't reappear;
  for a server deploy simply restores title (reappears next 5-min check)
- Auto-restores after 60 s without marking as seen
- Removed the old fixed position:fixed banner entirely
2026-05-04 05:41:38 +00:00
dadaloop82 968e26cc6a fix(ui): header title always centered, actions to right, real-time update detection
CSS header fixes:
- .header-content: justify-content:flex-end so .header-actions (camera, Gemini)
  naturally stays at the right edge as a flex child
- .header-title: removed overflow:hidden and text-overflow:ellipsis that were
  clipping the version number; title stays absolutely centered
- Cleaned up unused max-width:none and margin:0 from previous broken attempt

Real-time webapp update detection:
- Added module-level _loadedVersion captured at page load (version in HTML header)
- _checkWebappUpdate() now has two checks:
  1. webapp_version from server vs _loadedVersion: if different, the server was
     updated since this page was loaded → show '🔄 Nuova versione disponibile' banner
  2. GitHub latest release vs _loadedVersion (existing behaviour)
  Different banner messages: deploy-changed shows simple reload prompt;
  release-newer shows version + changelog link (same as before)
- TTL reduced from 6h to 5 min so updates are detected quickly
- _checkWebappUpdate() now also fires on visibilitychange so the user sees
  the banner as soon as they return to the tab after a deploy
2026-05-04 05:32:57 +00:00
dadaloop82 6756b16ecb fix(ui): center header title on tablet + add skeleton loader to spesa stat card
Header title centering:
- .header-content: remove max-width:600px, use position:relative + justify-content:center
- .header-title: position:absolute; left:50%; transform:translateX(-50%)
  so the title is always at the exact center of the header regardless of
  screen width or how many action buttons are on the right
- Added max-width:calc(100% - 200px) to prevent overlap with action buttons
  on narrow screens

Spesa skeleton preloader:
- index.html: add stat-loading class to stat-spesa (was missing, other 3 had it)
- app.js showPage('dashboard'): add 'spesa' to the skeleton init array
- app.js loadShoppingCount(): remove stat-loading class after data loads
  (like loadDashboard() does for the other 3 locations)
2026-05-04 05:26:38 +00:00
dadaloop82 a701e5a239 fix: 'Aggiorna ora' banner + APK conflict auto-retry after uninstall
Webapp update banner:
- 'Vedi novità' link replaced with 'Aggiorna ora' button
- Clicking 'Aggiorna ora' does a hard page reload (?bust=timestamp)
  which forces the browser to fetch the latest files from the server
- GitHub release URL kept as a small secondary 'novità' link

APK install conflict (kiosk + scale gateway):
- STATUS_PENDING_USER_ACTION: changed startActivity → startActivityForResult
  (kiosk) / installConfirmLauncher.launch (gateway) so we get notified
  if the system installer fails due to signature conflict
- On non-OK result from system installer: show AlertDialog offering to
  uninstall, using UNINSTALL_REQUEST / uninstallLauncher
- STATUS_FAILURE_CONFLICT/INCOMPATIBLE: same uninstall flow
- After uninstall completes, install automatically retries with the
  saved APK file (pendingInstallFile) — no manual re-download needed
- Gateway: also saves destFile to pendingInstallFile at download time
2026-05-03 18:22:29 +00:00
dadaloop82 eb2e8fa1d0 fix: _checkWebappUpdate ReferenceError — convert IIFE to function declaration
The function was wrapped as a named function expression (function foo(){})
which scopes the name 'foo' only to the function body, not the outer scope.
_initApp called setTimeout(_checkWebappUpdate, 6000) causing:
  ReferenceError: _checkWebappUpdate is not defined

Fix: remove the (...)​ wrapper so it becomes a regular function declaration
visible to the entire module scope.

Fixes #7
2026-05-03 18:14:15 +00:00
dadaloop82 73fbb73974 fix: APK install conflict (PackageInstaller) + dashboard stat skeleton
APK install conflict:
- Replace ACTION_VIEW-based install with PackageInstaller.Session API (API 21+)
- PackageInstaller gives us the actual install status via BroadcastReceiver:
  STATUS_PENDING_USER_ACTION → launch system confirmation dialog automatically
  STATUS_SUCCESS → success toast
  STATUS_FAILURE_CONFLICT/INCOMPATIBLE → show AlertDialog offering to
    uninstall the old version (ACTION_DELETE) so user can re-download and install
- FileProvider no longer needed for install (still kept for other uses)
- Kiosk: derive target package from filename (gateway vs kiosk self-update)

Dashboard 0-flash:
- Replace hardcoded 0 in HTML stat-value spans with ... placeholder
- Add .stat-loading CSS class: shimmer skeleton animation (gradient sweep)
- showPage(dashboard): set ... + stat-loading before API call
- loadDashboard: remove stat-loading class and set real count after data arrives
2026-05-03 17:51:18 +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 9ef2a53aeb fix: hide update banner + app-header during cooking mode; raise overlay z-index
- .cooking-overlay z-index 500 → 99998 (above everything)
- body.cooking-mode-active: hide #_evershelf_update_banner and .app-header
- .cooking-mode-active #modal-overlay z-index 600 → 99999
2026-05-03 17:33:24 +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 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 c814d99d1f feat: smart use-all context, scale baseline reset, freezer-ok alert suppression, conf qty fix, low-stock finish button
- submitUseAll() now detects opened packages: if current location has an opened pack, finishes only that; if exactly one opened pack exists elsewhere, uses it automatically; multiple opened packs → disambiguation modal
- quickUse() resets scale baseline on page open so stale weight doesn't immediately trigger auto-fill
- Expired alerts (dashboard + banner) now filter out freezer items within their safety window (level='ok')
- Review banner: conf unit quantity displayed as sub-unit total (e.g. 800g) instead of raw pack count; high-qty threshold evaluated on sub-unit volume to prevent '400 confezioni' nonsense
- Low-stock review banner gains 'È finito tutto' button → new bannerFinishAll() handler
- New _submitUseAllAt() helper and _showUseAllDisambiguation() modal
- New translation keys: toast_opened_finished, disambiguation_hint, disambiguation_all, banner_review_action_finish (it/en/de)
2026-05-03 13:12:35 +00:00
dadaloop82 4e583127dd Banner: suppress low-qty alert when sibling product entries exist elsewhere
A partially-used fridge entry (e.g. 191 ml of milk) triggered a
'suspiciously low quantity' banner even when sealed packages of the
same product were present in another location (e.g. pantry).

Fix: before pushing a low-qty review alert, group all inventory rows
by product key (barcode, or name+brand fallback). If any sibling entry
for the same product has qty > 0 in a different row, skip the alert.
High-qty and suspicious package-size alerts are unaffected.
2026-04-30 05:28:43 +00:00
dadaloop82 8359b14931 Banner: adapt expired icon/color/title to safety level (non-alarmist)
- ok level (long-life/freezer safe): green banner,  icon, 'Scaduto (ancora ok)'
- warning level: amber banner, 👀 icon, 'Scaduto (controlla)'
- danger level: unchanged red 🚫 banner
- Added banner-expired-ok / banner-expired-warning CSS variants
- Added expiry.expired_suffix_ok / expired_suffix_warning i18n keys (IT/EN/DE)
- Updated README and CHANGELOG
2026-04-30 05:21:50 +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 9c1346019c Waste section: neutral terminology, drop trend-cards & meals badge, annual/range in bar, 5-min facts, AW facts in screensaver 2026-04-29 16:27:05 +00:00
dadaloop82 67f58172e5 Waste: bigger fonts, auto-fit badge row, 5-min rotation 2026-04-29 16:19:54 +00:00
dadaloop82 85274948b4 Waste section: single stacked comparison bar instead of two rows 2026-04-29 16:13:43 +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 0f247a3132 Anti-waste: single-row compare bar, trend cards with arrows, rotating food facts 2026-04-29 06:11:53 +00:00
dadaloop82 0163ae11a2 Anti-waste: compact card, live dot, auto-refresh on connectivity 2026-04-29 06:01:14 +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 8722f15aa0 i18n: Translate all hardcoded Italian labels to English & German
- Convert LOCATIONS labels to use t('locations.*')
- Convert SHOPPING_SECTIONS labels to use t('shopping_sections.*')
- Convert CATEGORY_LABELS to use t('categories.*')
- Convert MEAL_PLAN_TYPES to use t('meal_plan_types.*')
- Convert WEEK_DAYS_SHORT to use t('days.*_short')
- Convert MEAL_TYPES to use t('meal_types.*')
- Convert MEAL_SUB_TYPES to use t('meal_sub.*')
- Convert meal-plan column headers to use translated meal_types
- Replace inline locLabels/LOC_LABELS with translated LOCATIONS object
- Fix shopping action buttons: bring_add_n, bring_add_selected, bring_adding, bring_added_*
- Fix recipe archive empty state
- Fix meal plan reset success toast
- Fix meal plan suggestion hint and screensaver display
- Fix settings save status messages (saved, saved_local, saved_local_error)
- Fix product edit form title
- Fix kiosk session phrases for screensaver counter
- Add cooking.expires_chip translation for expiry date format
- Add meal_plan section (reset_success, suggested_by)
- Add error.select_items for Bring shopping validation
- All strings now properly internationalized for EN/DE languages
2026-04-28 16:03:07 +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 8a16307b39 i18n: translate all hardcoded Italian strings in app.js
Added 49 new translation keys to all 3 language files (IT/EN/DE)
and wired every hardcoded Italian label/toast/hint in app.js to use
the t() translation function.

Sections covered:
- scale: density_hint, ml_hint, weight_detected, weight_too_low,
         stable, auto_confirm
- dashboard: banner review titles/details, prediction rate/days/
             direction texts, finished-zero/expected/check,
             anomaly phantom/ghost titles and details
- action: have_title, add_more_sub, use_qty_sub, throw_btn/sub, edit_sub
- add: purchase_type_label, new_btn, existing_btn,
       remaining_label/hint/full/half
- use: throw_title, throw_all, throw_qty_label/hint, throw_partial_btn
- shopping: bring_badge, add_urgent_toast, migration_done,
            added_to_bring, added_to_bring_skip, all_on_bring,
            removed_sufficient (was a complex plural, now uses key)
- toast: product_updated, thrown_away, thrown_away_partial
- confirm: kiosk_exit
- WEEK_DAYS array now uses t('days.*') keys
2026-04-28 06:36:30 +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 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 95389ebe87 fix: native Android TTS bridge in kiosk — bypass Web Speech API voice issues
On Android WebView, window.speechSynthesis.getVoices() often returns empty
because the Web Speech API cannot enumerate the device's TTS voices.
This caused the kiosk to show 'nessuna voce offline è supportata'.

Changes:
- KioskActivity.kt: initialise Android TextToSpeech engine on startup;
  expose speak(text, rate, pitch), stopSpeech() and isTtsReady() via
  the existing _kioskBridge JavascriptInterface; release TTS in onDestroy.
- app.js (_speakBrowser): when _kioskBridge.speak is available, delegate
  to it instead of using speechSynthesis — works even without offline voice
  packs installed.
- app.js (_initBrowserTtsVoices): show 'Voce nativa Android (kiosk)'
  in the voice dropdown when running inside the kiosk WebView.
- app.js (testTTS): use the bridge path when testing TTS inside the kiosk.
2026-04-27 11:52:30 +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 61e94db0d3 style: barcode suffix in banner title smaller + monospace 2026-04-27 05:54:28 +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 37299e60c9 fix: TTS voices — retry 10s, message on failure, refresh button
- Retry loop extended to 10s (50×200ms) for slow Android WebViews
- Show 'Nessuna voce disponibile' after timeout instead of infinite loader
- Show 'Voce non supportata dal browser' if speechSynthesis missing
- Reset to loading state on each settings open (fixes stale empty select)
- Add refresh button ↺ to force-reload voices manually
2026-04-26 17:28:05 +00:00
dadaloop82 f57ad4b330 fix: TTS voice selector stuck on 'Caricamento voci' in Chrome
Chrome loads voices async — getVoices() returns [] on first call
and onvoiceschanged may fire before the handler is assigned (race).
Add fallback retry loop: poll every 200ms for up to 4s.
2026-04-26 17:23:39 +00:00
dadaloop82 fe0221e6d4 fix: banner buttons — no icons, qty in confirm, full i18n 2026-04-26 17:17:05 +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 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