From 75ca49ac4ee5575ea06853ccc10c45702da6fa37 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Sun, 10 May 2026 13:18:41 +0000 Subject: [PATCH] fix: smart shopping family suppression, shelf life pre-warming (v1.7.7) - Remove recentlyExhausted bypass from shopping_name family suppression: products recently exhausted (<14d) were incorrectly flagged as critical even when the same family had ample stock (Yogurt 2002g, Affettato 1022g, Pane 400g). recentlyExhausted now only bypasses loose token-based coverage. - Add prewarmShelfLifeCache() in cron: pre-warms opened shelf life via Gemini AI (max 5 items/cycle) so the UI never blocks on first load. --- CHANGELOG.md | 6 ++++++ api/cron_smart_shopping.php | 13 ++++++++++++ api/index.php | 41 +++++++++++++++++++++++++++++++++++-- manifest.json | 2 +- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab7d83..8469e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to EverShelf will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.7] - 2026-05-10 + +### Fixed +- **Smart shopping family suppression** — La logica `recentlyExhausted` (prodotti terminati < 14gg) bypassava erroneamente anche la suppression per `shopping_name` family, causando falsi positivi: prodotti come Yaourt Vanille apparivano come urgenti anche con 2kg di Yogurt in stock, Salame Paesano con 1kg di Affettato in stock, Gran bauletto rustico con più pani in stock. Ora `recentlyExhausted` bypassa solo il check token-based (match lasco), mentre la family suppression per `shopping_name` si applica sempre. +- **Shelf life pre-warming nel cron** — Il cron ora chiama `prewarmShelfLifeCache()` ogni 5 minuti, precaricando via Gemini AI la shelf life degli item aperti in inventario (max 5 item per ciclo) prima che l'utente li visualizzi. Questo elimina il delay percepibile al primo click su "Aperto il...". + ## [1.7.6] - 2026-05-10 ### Fixed diff --git a/api/cron_smart_shopping.php b/api/cron_smart_shopping.php index 7c6bb2a..867f777 100644 --- a/api/cron_smart_shopping.php +++ b/api/cron_smart_shopping.php @@ -66,6 +66,19 @@ try { echo '[' . date('Y-m-d H:i:s') . '] Bring! sync warning: ' . $be->getMessage() . "\n"; } + // ── Shelf life pre-warming ──────────────────────────────────────────── + // Pre-warm the opened shelf life cache for opened items not yet cached. + // Capped at 5 items per cron cycle to avoid Gemini rate limits. + try { + $prewarmResult = prewarmShelfLifeCache($db, 5); + if ($prewarmResult['warmed'] > 0) { + echo '[' . date('Y-m-d H:i:s') . '] Shelf life pre-warm — warmed: ' . $prewarmResult['warmed'] + . ', skipped: ' . $prewarmResult['skipped'] . "\n"; + } + } catch (Throwable $pe) { + echo '[' . date('Y-m-d H:i:s') . '] Shelf life pre-warm warning: ' . $pe->getMessage() . "\n"; + } + } catch (Throwable $e) { $msg = $e->getMessage(); echo '[' . date('Y-m-d H:i:s') . '] ERROR: ' . $msg . "\n"; diff --git a/api/index.php b/api/index.php index 1790aa8..a7ecde7 100644 --- a/api/index.php +++ b/api/index.php @@ -2386,6 +2386,42 @@ function callGeminiWithFallback(string $apiKey, array $payload, int $timeout = 3 // ===== AI-POWERED OPENED SHELF LIFE ===== +/** + * Cron helper: pre-warm the opened shelf life cache for opened inventory items that + * have no cache entry yet. Called once per cron cycle; capped to $limit items to + * avoid blocking or hitting Gemini rate limits. + * Returns ['warmed' => int, 'skipped' => int]. + */ +function prewarmShelfLifeCache(PDO $db, int $limit = 5): array { + $cacheFile = __DIR__ . '/../data/opened_shelf_cache.json'; + $cache = []; + if (file_exists($cacheFile)) { + $cache = json_decode(file_get_contents($cacheFile), true) ?: []; + } + + // Fetch opened items from inventory (only those still with quantity > 0) + $rows = $db->query(" + SELECT p.name, p.category, i.location + FROM inventory i + JOIN products p ON p.id = i.product_id + WHERE i.opened_at IS NOT NULL AND i.quantity > 0 + ORDER BY i.opened_at ASC + ")->fetchAll(PDO::FETCH_ASSOC); + + $warmed = 0; + $skipped = 0; + foreach ($rows as $row) { + if ($warmed >= $limit) { $skipped++; continue; } + $cacheKey = md5(mb_strtolower($row['name']) . '|' . mb_strtolower($row['location'])); + if (isset($cache[$cacheKey])) { $skipped++; continue; } + // Call with AI enabled — this writes to cache internally + getOpenedShelfLifeDays($row['name'], $row['category'] ?? '', $row['location'], false, true); + $warmed++; + } + + return ['warmed' => $warmed, 'skipped' => $skipped]; +} + /** * Return the number of days a product remains safe after opening, depending on storage location. * Checks a local JSON cache first (keyed by product name+location); on cache miss, asks Gemini AI. @@ -5655,8 +5691,9 @@ function smartShopping(PDO $db): void { // (e.g. "Formaggio") and there's stock of ANY product with the same generic name, // the need is covered. This catches "Bel Paese" → covered by "Formaggio Gouda" in stock, // "Biscotti Pastefrolle" → covered by "Frollini..." (both shopping_name="Biscotti"), etc. - // Exception: recently exhausted products (< 14 days) skip this suppression too. - if (!$coveredByEquivalent && !$recentlyExhausted) { + // NOTE: recentlyExhausted does NOT bypass this check — same-family stock always suppresses. + // recentlyExhausted only bypasses the loose token-based check above. + if (!$coveredByEquivalent) { $sName = strtolower(trim($p['shopping_name'] ?? '')); if ($sName !== '' && ($stockByShoppingName[$sName] ?? 0) > 0) { $coveredByEquivalent = true; diff --git a/manifest.json b/manifest.json index ccd7b2c..ce227bb 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "name": "EverShelf", "short_name": "EverShelf", "description": "Gestione completa della dispensa di casa con scansione barcode", - "version": "1.7.6", + "version": "1.7.7", "start_url": "/evershelf/", "display": "standalone", "background_color": "#f0f4e8",