Conservative Bring! cleanup + operations log

- cleanupObsoleteBringItems() now much more conservative:
  * Only removes items matching a known DB product (preserves manual additions)
  * Only removes if the product has current_qty > 0 (has stock)
  * AND item is no longer flagged by smart shopping
- Added logOperation() — stores all Bring! operations in localStorage '_opLog'
  (bring_auto_add, bring_cleanup, bring_found, bring_manual_remove)
  Capped at 200 entries, each with timestamp + action + details
- All Bring! add/remove paths now log their operations
This commit is contained in:
dadaloop82
2026-03-30 13:36:51 +00:00
parent b954be4cac
commit c63faf56e4
3 changed files with 51 additions and 15 deletions
+50 -14
View File
@@ -4310,6 +4310,7 @@ async function confirmShoppingItemFound() {
const idx = shoppingItems.findIndex(i => i.name.toLowerCase() === name.toLowerCase()); const idx = shoppingItems.findIndex(i => i.name.toLowerCase() === name.toLowerCase());
if (idx >= 0) shoppingItems.splice(idx, 1); if (idx >= 0) shoppingItems.splice(idx, 1);
showToast(`${name} rimosso dalla lista!`, 'success'); showToast(`${name} rimosso dalla lista!`, 'success');
logOperation('bring_found', { name });
loadShoppingCount(); loadShoppingCount();
} }
} catch (e) { console.error('confirmShoppingItemFound', e); } } catch (e) { console.error('confirmShoppingItemFound', e); }
@@ -4327,42 +4328,61 @@ async function autoAddCriticalItems() {
const result = await api('bring_add', {}, 'POST', { items: itemsToAdd, listUUID: shoppingListUUID }); const result = await api('bring_add', {}, 'POST', { items: itemsToAdd, listUUID: shoppingListUUID });
if (result.success && result.added > 0) { if (result.success && result.added > 0) {
showToast(`🔴 ${result.added} prodott${result.added === 1 ? 'o urgente aggiunto' : 'i urgenti aggiunti'} automaticamente a Bring!`, 'success'); showToast(`🔴 ${result.added} prodott${result.added === 1 ? 'o urgente aggiunto' : 'i urgenti aggiunti'} automaticamente a Bring!`, 'success');
logOperation('bring_auto_add', { added: itemsToAdd.map(i => i.name) });
loadShoppingList(); loadShoppingList();
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
} }
/** /**
* One-time cleanup: remove items from Bring! that the new smart algorithm no longer considers relevant. * One-time cleanup: remove items from Bring! that were auto-added but the algorithm no
* Compares current shoppingItems vs smartShoppingItems if a Bring! item has NO match in * longer considers relevant. CONSERVATIVE: only removes items that match a known product
* smart predictions (or matches a product with urgency 'none' that was filtered out), remove it. * in our inventory with current_qty > 0 AND that no longer appear in smart predictions.
* Items not matching any DB product are left untouched (likely manually added by user).
*/ */
async function cleanupObsoleteBringItems() { async function cleanupObsoleteBringItems() {
if (sessionStorage.getItem('_bringCleanupDone')) return; if (sessionStorage.getItem('_bringCleanupDone')) return;
sessionStorage.setItem('_bringCleanupDone', '1'); sessionStorage.setItem('_bringCleanupDone', '1');
if (!shoppingItems.length || !smartShoppingItems.length) return; if (!shoppingItems.length || !smartShoppingItems.length) return;
// Build a set of names that the smart algorithm still considers relevant // Build set of smart-flagged names (these should stay)
const smartNames = new Set(smartShoppingItems.map(i => i.name.toLowerCase())); const smartNames = new Set(smartShoppingItems.map(i => i.name.toLowerCase()));
// Find Bring! items that have NO match in smart predictions AND are not manually added // Load all products from our DB to cross-reference
// (We consider items "auto-added" if their name matches a known product in our DB) let allProducts = [];
try {
const res = await api('products');
allProducts = res.products || res.data || [];
} catch (e) { return; }
if (!allProducts.length) return;
// Index our products by lowercase name
const dbByName = {};
for (const p of allProducts) {
dbByName[p.name.toLowerCase()] = p;
}
const toRemove = []; const toRemove = [];
for (const item of shoppingItems) { for (const item of shoppingItems) {
const nameLower = item.name.toLowerCase(); const nameLower = item.name.toLowerCase();
// If smart shopping still flags this item, keep it // If smart shopping still flags it, keep it
if (smartNames.has(nameLower)) continue; if (smartNames.has(nameLower)) continue;
// Check with token matching too if (_findSimilarItem(item.name, smartShoppingItems)) continue;
const smartMatch = _findSimilarItem(item.name, smartShoppingItems);
if (smartMatch) continue; // Must match a known DB product — otherwise it's likely a manual addition
// This item is no longer predicted by smart shopping — candidate for removal const dbProduct = dbByName[nameLower] || _findSimilarItem(item.name, allProducts);
if (!dbProduct) continue;
// Only remove if we have stock (qty > 0) — if qty == 0 user may still need it
if ((dbProduct.quantity || 0) <= 0) continue;
toRemove.push(item); toRemove.push(item);
} }
if (toRemove.length === 0) return; if (toRemove.length === 0) return;
// Remove them from Bring!
let removed = 0; let removed = 0;
const removedNames = [];
for (const item of toRemove) { for (const item of toRemove) {
try { try {
const r = await api('bring_remove', {}, 'POST', { const r = await api('bring_remove', {}, 'POST', {
@@ -4370,16 +4390,31 @@ async function cleanupObsoleteBringItems() {
rawName: item.rawName || '', rawName: item.rawName || '',
listUUID: shoppingListUUID listUUID: shoppingListUUID
}); });
if (r.success) removed++; if (r.success) { removed++; removedNames.push(item.name); }
} catch (e) { /* ignore individual failures */ } } catch (e) { /* ignore individual failures */ }
} }
if (removed > 0) { if (removed > 0) {
showToast(`🧹 ${removed} prodott${removed === 1 ? 'o non più necessario rimosso' : 'i non più necessari rimossi'} dalla lista`, 'info'); showToast(`🧹 ${removed} prodott${removed === 1 ? 'o con scorte sufficienti rimosso' : 'i con scorte sufficienti rimossi'} dalla lista`, 'info');
logOperation('bring_cleanup', { removed: removedNames });
loadShoppingList(); loadShoppingList();
} }
} }
/**
* Log an app operation (not a food transaction) for auditing/debugging.
* Stored in localStorage under '_opLog', capped at 200 entries.
*/
function logOperation(action, details) {
try {
const log = JSON.parse(localStorage.getItem('_opLog') || '[]');
log.push({ ts: new Date().toISOString(), action, details });
// Keep last 200 entries
if (log.length > 200) log.splice(0, log.length - 200);
localStorage.setItem('_opLog', JSON.stringify(log));
} catch (e) { /* ignore */ }
}
const DEFAULT_SPESA_AI_PROMPT = `Sei un assistente per la spesa online. Ti viene dato il nome di un prodotto che l'utente vuole comprare e una lista di prodotti trovati nel catalogo del supermercato. const DEFAULT_SPESA_AI_PROMPT = `Sei un assistente per la spesa online. Ti viene dato il nome di un prodotto che l'utente vuole comprare e una lista di prodotti trovati nel catalogo del supermercato.
Regole di selezione: Regole di selezione:
@@ -5092,6 +5127,7 @@ async function removeBringItem(idx) {
shoppingItems.splice(idx, 1); shoppingItems.splice(idx, 1);
renderShoppingItems(); renderShoppingItems();
showToast('Rimosso dalla lista', 'success'); showToast('Rimosso dalla lista', 'success');
logOperation('bring_manual_remove', { name: item.name });
// Update dashboard shopping count // Update dashboard shopping count
loadShoppingCount(); loadShoppingCount();
} }
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -997,6 +997,6 @@
</div> </div>
</div> </div>
<script src="assets/js/app.js?v=20260329e"></script> <script src="assets/js/app.js?v=20260330a"></script>
</body> </body>
</html> </html>