From 1fb00d48a951ae723be2e400db947d549952b9f1 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 4 May 2026 16:34:34 +0000 Subject: [PATCH] fix(shopping): prevent cleanup from removing user-manually-added items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cleanupObsoleteBringItems now protects items the user explicitly added from the suggestions panel via a '_userPinnedBring' localStorage set (30-day TTL, cleared on force-sync) - cleanup now protects ALL smart-predicted items (any urgency), not only critical/high — if the algorithm still flags it, it should stay in list - autoAddCriticalItems: bypass purchased-blocklist for depleted items (current_qty=0) so products that ran out are always re-added to Bring - forceSyncBring also clears _userPinnedBring for a full reset --- assets/js/app.js | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/assets/js/app.js b/assets/js/app.js index 4f4df44..bc43093 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -8006,7 +8006,8 @@ async function autoAddCriticalItems() { if (i.on_bring) return false; // For imminent items, do not honor local "purchased" blocklist too aggressively. // If they are predicted to finish within a week, keep Bring aligned automatically. - if (!imminentWeek && _isBringPurchased(i.name, i.urgency)) return false; + // Bypass blocklist for depleted items (current_qty=0) — they ran out and must be re-added + if (!imminentWeek && (i.current_qty ?? 0) > 0 && _isBringPurchased(i.name, i.urgency)) return false; if (i.urgency === 'critical') return true; if (i.urgency === 'high') return true; if (i.urgency === 'medium' && (i.days_left ?? 999) <= 7 && (i.uses_per_month || 0) >= 3) return true; @@ -8036,6 +8037,7 @@ async function forceSyncBring() { localStorage.removeItem('_bringPurchasedBlocklist'); localStorage.removeItem('_autoAddedCriticalTs'); localStorage.removeItem('_bringCleanupTs'); + localStorage.removeItem('_userPinnedBring'); logOperation('force_sync_bring', {}); // Reload everything from scratch await loadShoppingList(); @@ -8075,15 +8077,29 @@ async function cleanupObsoleteBringItems() { } } - // Build: any matching token → smart item (critical/high only) - const urgentSmartByToken = new Map(); + // Build: any matching token → smart item (any urgency — all predictions are protected) + const smartByToken = new Map(); for (const si of smartShoppingItems) { - if (si.urgency !== 'critical' && si.urgency !== 'high') continue; for (const tok of _nameTokens(si.name)) { - if (!urgentSmartByToken.has(tok)) urgentSmartByToken.set(tok, si); + if (!smartByToken.has(tok)) smartByToken.set(tok, si); } } + // User-pinned: items manually added via the suggestions panel — never auto-remove + let userPinned; + try { + const raw = localStorage.getItem('_userPinnedBring'); + const map = raw ? JSON.parse(raw) : {}; + // Prune entries older than 30 days + const now = Date.now(); + let changed = false; + for (const k of Object.keys(map)) { + if (now - map[k] > 30 * 24 * 60 * 60 * 1000) { delete map[k]; changed = true; } + } + if (changed) localStorage.setItem('_userPinnedBring', JSON.stringify(map)); + userPinned = map; + } catch(e) { userPinned = {}; } + const toRemove = []; for (const item of shoppingItems) { // Check if any significant token of this Bring item has stock in inventory @@ -8093,13 +8109,14 @@ async function cleanupObsoleteBringItems() { // No inventory stock for any related product → nothing to remove if (stockQty <= 0) continue; - // Check if smart shopping flags something with a matching token as urgently needed - const urgSi = itemTokens.map(tok => urgentSmartByToken.get(tok)).find(Boolean); - if (urgSi) { - // Smart says something with this root token is urgent. - // If the flagged product still has qty > 0, it's genuinely running low → keep. - // If depleted (qty=0) but we have equivalent stock via another token → remove. - if (urgSi.current_qty > 0) continue; + // Never remove items the user explicitly pinned from suggestions + if (userPinned[item.name.toLowerCase()]) continue; + + // Check if smart shopping flags something with a matching token as needed (any urgency) + const smartSi = itemTokens.map(tok => smartByToken.get(tok)).find(Boolean); + if (smartSi) { + // Smart still predicts this item will be needed and it has remaining stock → keep it + if (smartSi.current_qty > 0) continue; } toRemove.push(item); @@ -8497,6 +8514,13 @@ async function addSmartToBring() { ? t('shopping.added_to_bring', { n: result.added }) + (result.skipped > 0 ? ` (${t('shopping.added_to_bring_skip', { n: result.skipped })})` : '') : t('shopping.all_on_bring'); showToast(msg, result.added > 0 ? 'success' : 'info'); + // Mark all manually-added items as user-pinned so cleanupObsoleteBringItems never removes them + if (result.added > 0) { + const pinned = JSON.parse(localStorage.getItem('_userPinnedBring') || '{}'); + const now = Date.now(); + for (const it of itemsToAdd) pinned[it.name.toLowerCase()] = now; + localStorage.setItem('_userPinnedBring', JSON.stringify(pinned)); + } // Reload to refresh badges loadShoppingList(); } else {