diff --git a/README.md b/README.md index 972e158..68d92a0 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ - **Smart predictions** — Know what you'll need before you run out - **Auto-add on depletion** — When a product reaches zero the app adds it to Bring! automatically, no confirmation needed - **Auto-remove on scan** — Products are removed from the shopping list when scanned in - **Auto-migration** — Items already on the Bring! list are silently renamed to their generic name in the background (throttled, runs on list load) - - **Catalog coverage** — All product types resolve to a German Bring! catalog key for icon and category display in the Bring! app- **DupliClick integration** — Online grocery ordering (Gruppo Poli) + - **Catalog coverage** — All product types resolve to a German Bring! catalog key for icon and category display in the Bring! app ### 🍳 Cooking Mode - **Step-by-step guidance** — Follow recipes with a hands-free cooking interface diff --git a/api/index.php b/api/index.php index 59e1c7d..3ea6833 100644 --- a/api/index.php +++ b/api/index.php @@ -1691,6 +1691,38 @@ function getStats(PDO $db): void { } $item['is_edible'] = $item['days_to_expiry'] === null || $item['days_to_expiry'] >= 0; $item['has_opened_at'] = !empty($item['opened_at']); + + // For conf items with opened_at that contain both whole and fractional confs: + // split into a "sealed" entry (whole confs, package expiry) and an "opened" entry (fraction, shelf-life expiry). + // This prevents a row like "1.59 conf" from showing a single misleading entry that mixes + // a still-sealed package with an opened portion. + if ($item['unit'] === 'conf' && $item['has_opened_at'] && $originalExpiry !== null) { + $qty = (float)$item['quantity']; + $whole = (int)floor($qty + 0.001); + $frac = round($qty - (float)$whole, 4); + if ($whole >= 1 && $frac >= 0.001) { + // Sealed whole confs: show with original package expiry (only if near expiry ≤ 7 d) + $sealedDays = (int)round(($originalExpiry - $today) / 86400); + if ($sealedDays <= 7 && $sealedDays >= -30) { + $si = $item; + $si['quantity'] = (float)$whole; + $si['opened_at'] = null; + $si['opened_expiry'] = date('Y-m-d', $originalExpiry); + $si['days_to_expiry'] = $sealedDays; + $si['is_edible'] = $sealedDays >= 0; + $si['has_opened_at'] = false; + $opened[] = $si; + } + // Opened fractional part: use the already-computed opened shelf-life expiry + if ($item['days_to_expiry'] === null || $item['days_to_expiry'] <= 365) { + $fi = $item; + $fi['quantity'] = $frac; + $opened[] = $fi; + } + continue; + } + } + // Hide non-perishable items (salt, sugar, spirits, oil, etc.) — they won't expire usefully if ($item['days_to_expiry'] !== null && $item['days_to_expiry'] > 365) continue; // Hide legacy fractional items (no opened_at) with far-off expiry — not useful for home widget @@ -2155,7 +2187,10 @@ function getOpenedShelfLifeDays(string $name, string $category, string $location if ($result['http_code'] === 200) { $text = trim($result['data']['candidates'][0]['content']['parts'][0]['text'] ?? ''); $parsed = (int)preg_replace('/\D/', '', $text); - if ($parsed > 0 && $parsed <= 3650) { + // Reject AI values if they are suspiciously low compared to the rule-based estimate + // (protects against Gemini hallucinations like "1 day for butter"). + $ruleMin = estimateOpenedExpiryDaysPHP($name, $category, $location); + if ($parsed > 0 && $parsed <= 3650 && $parsed >= max(1, (int)floor($ruleMin * 0.5))) { $days = $parsed; } } diff --git a/assets/js/app.js b/assets/js/app.js index 9d42b99..8af800d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -2529,7 +2529,7 @@ async function loadDashboard() { if (statsData.opened && statsData.opened.length > 0) { // Sorted server-side by days_to_expiry ASC openedSection.style.display = 'block'; - const MAX_SHOWN = 10; + const MAX_SHOWN = 20; const visible = statsData.opened.slice(0, MAX_SHOWN); const extra = statsData.opened.length - visible.length; openedList.innerHTML = visible.map(item => { diff --git a/data/opened_shelf_cache.json b/data/opened_shelf_cache.json new file mode 100644 index 0000000..9f18f7f --- /dev/null +++ b/data/opened_shelf_cache.json @@ -0,0 +1,44 @@ +{ + "226887def70e33ef73290ebfe75ed4d0": { + "days": 7, + "source": "ai", + "name": "Polpa di pomodoro finissima", + "location": "frigo", + "ts": 1777444819 + }, + "0ed51c9496aa9edfe38caf41772f54ed": { + "days": 7, + "source": "rule", + "name": "Latte di Montagna", + "location": "frigo", + "ts": 1777444820 + }, + "2d63d0216a75d46b465150e925d2e7ad": { + "days": 30, + "source": "rule", + "name": "Burro", + "location": "frigo", + "ts": 1777444821 + }, + "f6504a014f17457e3dbe0ba917ad681f": { + "days": 7, + "source": "rule", + "name": "Latte di Montagna", + "location": "dispensa", + "ts": 1777444888 + }, + "7b15356b493402e17fa417a389e89716": { + "days": 60, + "source": "rule", + "name": "Yaourt Vanille", + "location": "dispensa", + "ts": 1777472391 + }, + "9afdf35c4a256867ef47c32495349eb6": { + "days": 5, + "source": "rule", + "name": "Yaourt Vanille", + "location": "frigo", + "ts": 1777480477 + } +} \ No newline at end of file diff --git a/index.html b/index.html index ca49874..7f44b00 100644 --- a/index.html +++ b/index.html @@ -90,15 +90,16 @@

🚫 Scaduti

+ + + + - - -